Repository navigation
fix(schema): keep tuple elements around defaults in .in and .out - #1696
Merged
Merged
Conversation
A tuple's defaultables are converted to optionals on input and prefix elements on output, but reduceIo assigned them over the existing optionals or prefix instead of combining them. Depending on key order, this dropped either the defaultable or the surrounding elements, e.g. [string, number = 5, boolean?] had an input of [string, boolean?] and [number, string = "foo", ...number[]] had an output of number[].
Contributor
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — verified the tuple reduceIo composition fix and its regression tests against main (c34bf69).
- Tuple
reduceIocomposes instead of overwriting —prefix,optionals, anddefaultablesnow merge intoinner.prefix/inner.optionalsin tuple order (prefix then defaultables on output; defaultables then optionals on input), so elements adjacent to a default are no longer dropped. The order-independence claim holds:prefixprepends,defaultablesprepends onin/appends onout, andoptionalsappends, so any key reduction order yields the same result. - Regression tests —
[string, number = 5, boolean?]and[number, string = 'foo', ...number[]]assert.in/.outexpressions, inferred types, andallows.
I reproduced the bug on the base commit (e.g. .in/.out both yielded [string, boolean?] for the first case, and .out collapsed to number[] for the second) and confirmed both are corrected. The ["number", "string = 'foo'"].out throw mentioned in the description is genuinely pre-existing — it throws on base too (with a <= 1 refinement message), so this PR does not introduce it. No regressions in the full suite (1793 passing).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…nother
A bound can narrow two existing constraints at once: >= 2 meets <= 2 as
== 2 and >= 1 as >= 2. For numbers and Dates the first result is a unit,
but for lengths it is an exactLength constraint, so intersectConstraints
threw "multiple distinct intersection results" for
type("1 <= string <= 2").and("string >= 2").
The second narrowed constraint is now removed and its result queued to
be intersected with the rest. That also reduces
type("1 <= string <= 3").and("string == 2") to string == 2 instead of
leaving == 2 & >= 1. Together with the tuple reduceIo fix,
type(["number", "string = 'foo'"]).out is now [number, string]; on its
own, that case would silently reduce to [number].
From arktypeio#1697. The tuple reduceIo change also drops the merges inside
defaultables, which never ran: inner keys are sorted, so defaultables
always reduces before optionals and prefix.
Co-Authored-By: ybai08 <271969568+ybai08@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ssalbdivad
added a commit
that referenced
this pull request
Oct 20, 2026
A tuple's defaultables become optionals on input and prefix elements on output, but they reduced first (inner keys are sorted) and prefix and optionals then overwrote them with their own rawIn/rawOut, so type(["string", "number = 5", "boolean?"]).in was [string, boolean?] and type(["number", "string = 'foo'"]).out was [number]. prefix now prepends to what defaultables wrote and optionals appends to it. Ported from #1696. Co-Authored-By: ybai08 <271969568+ybai08@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Member
ssalbdivad
added a commit
that referenced
this pull request
Oct 20, 2026
Bumps all publishable packages and documents the changes merged since 2.2.8: - keep tuple elements around defaults in .in and .out (#1696, @ybai08) - reject NaN in unions discriminated by typeof (#1702, @thecotne) - don't treat Object.prototype keys as declared (#1701, @thecotne) - only emit referenced $defs in toJsonSchema (#1689, fixes #1612, #1511 and #1546, @giaBaoJS) - convert unrepresentable types in ~standard.jsonSchema instead of throwing (#1694) - arkregex: allow } to be escaped (#1691, @cpruijsen) - attest: onlyNamedTestFiles (#1688) Also moves attest to TypeScript 7.1's stable native API. Starting with 7.1.0-dev.20261009.1, typescript/unstable/{ast,sync,fs} are exported as typescript/{ast,sync,fs}, so the published 0.57.0 fails on every current 7.1 build with "Package subpath './unstable/ast' is not defined by exports". The tsconfig paths aliases for the old specifiers kept resolving them, so typecheck stayed green while attest crashed at runtime. They are renamed alongside the imports. The workspace TypeScript moves to 7.1.0-dev.20261020.3, the latest nightly (no 7.1 beta is published yet). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ssalbdivad
added a commit
that referenced
this pull request
Oct 20, 2026
Bumps all publishable packages and documents the changes merged since 2.2.8: - keep tuple elements around defaults in .in and .out (#1696, @ybai08) - reject NaN in unions discriminated by typeof (#1702, @thecotne) - don't treat Object.prototype keys as declared (#1701, @thecotne) - only emit referenced $defs in toJsonSchema (#1689, fixes #1612, #1511 and #1546, @giaBaoJS) - convert unrepresentable types in ~standard.jsonSchema instead of throwing (#1694) - arkregex: allow } to be escaped (#1691, @cpruijsen) - attest: onlyNamedTestFiles (#1688) Also moves attest to TypeScript 7.1's native API paths. Starting with 7.1.0-dev.20261009.1, typescript/unstable/{ast,sync,fs} are exported as typescript/{ast,sync,fs}, so the published 0.57.0 fails on every current 7.1 build with "Package subpath './unstable/ast' is not defined by exports". The tsconfig paths aliases for the old specifiers kept resolving them, so typecheck stayed green while attest crashed at runtime. They are renamed alongside the imports. The workspace TypeScript moves to 7.1.0-beta, which exports the same paths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ssalbdivad
added a commit
that referenced
this pull request
Oct 20, 2026
Bumps all publishable packages and documents the changes merged since 2.2.8: - keep tuple elements around defaults in .in and .out (#1696, @ybai08) - reject NaN in unions discriminated by typeof (#1702, @thecotne) - don't treat Object.prototype keys as declared (#1701, @thecotne) - only emit referenced $defs in toJsonSchema (#1689, fixes #1612, #1511 and #1546, @giaBaoJS) - convert unrepresentable types in ~standard.jsonSchema instead of throwing (#1694) - arkregex: allow } to be escaped (#1691, @cpruijsen) - attest: onlyNamedTestFiles (#1688) Also moves attest to TypeScript 7.1's native API paths. Starting with 7.1.0-dev.20261009.1, typescript/unstable/{ast,sync,fs} are exported as typescript/{ast,sync,fs}, so the published 0.57.0 fails on every current 7.1 build with "Package subpath './unstable/ast' is not defined by exports". The tsconfig paths aliases for the old specifiers kept resolving them, so typecheck stayed green while attest crashed at runtime. They are renamed alongside the imports. The workspace TypeScript moves to 7.1.0-beta, which exports the same paths.
ssalbdivad
added a commit
that referenced
this pull request
Oct 21, 2026
Ports the tests of 092075a's second fix from arktype 2.2.9 (#1696, from #1697, @ybai08). There, a bound narrowing two constraints at once, as `>= 2` meets `<= 2` as `== 2` and `>= 1` as `>= 2`, threw "multiple distinct intersection results" for lengths, where the first result is an exactLength rather than a unit: type("1 <= string <= 2").and("string >= 2") This branch already intersects the second result again (d00d9dd), tested only through a tuple intersecting itself. The range and bounds tests come over unchanged and pass. The commit's tuple .in/.out fix came with featherduster (540c78e). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ssalbdivad
added a commit
that referenced
this pull request
Oct 21, 2026
main contributes the 2.2.9 release: version bumps for every publishable
package (arktype 2.2.9, @ark/schema, @ark/util and @ark/fs 0.56.8, attest
0.57.1, arkregex 0.0.14, @ark/json-schema 0.0.13, @ark/fast-check 0.0.20),
attest's move to TypeScript 7.1's native API paths (typescript/{ast,sync,fs}
in place of typescript/unstable/*, with the workspace catalog on
7.1.0-beta), arkdark's regex() highlighting and CI/icon changes, and the
2.2.9 fixes: tuple elements around defaults in .in/.out (#1696), the NaN
check in typeof-discriminated unions (#1702), Object.prototype keys as
undeclared (#1701), only referenced $defs in toJsonSchema (#1689) and
escaped } in arkregex (#1691).
featherduster already had an equivalent of every one of those fixes, each
with main's test or a broader one, so every code conflict keeps
featherduster's side:
- ark/schema constraint.ts, roots/{domain,root,union}.ts,
structure/{sequence,structure}.ts, shared/toJsonSchema.ts: main patched
code that featherduster has moved to ark/sets (intersectConstraints,
discrimination, toJsonSchema) or rewritten. featherduster already narrows
two constraints with one bound, reduces tuple prefix/optionals around
defaultables, keeps the NaN check on a typeof case, uses a null-prototype
propsByKey and resolves only referenced $defs.
- ark/type/__tests__/discrimination.test.ts: main's "number case includes
NaN" is covered by featherduster's "number case includes NaN and
Infinity". Its jitless loop is dropped, since testJitless runs the whole
suite jitless.
- ark/type/__tests__/keywords/number.test.ts: featherduster's messages
("must be a number (was NaN)") are kept. A miss of number | null has the
discriminant code there, so it names only the branch the value reached.
- ark/type/__tests__/match.test.ts: featherduster's caseReference snapshots.
- ark/type/__tests__/toJsonSchema.test.ts: featherduster's referencedDefs
tests cover main's three useRefs cases (#1546, #1511, #1612).
- CHANGELOGs: featherduster already carried arktype's 2.2.9 and arkregex's
0.0.14 sections, identical to main's, below its Unreleased sections. In
attest's, main's 0.57.1 section goes below Unreleased.
- pnpm-lock.yaml: featherduster's, reinstalled against the merged
catalog, so the only change is typescript 7.1.0-dev.20261006.1 ->
7.1.0-beta.
The precompiled-module version link moves with the version: runtime.ts
exports precompiledForArktype_2_2_9 in place of precompiledForArktype_2_2_8,
which build.ts's checkVersionExport derives from package.json. The
precompile setup and troubleshooting docs, and llms-full.txt generated
from them, name 2_2_9 and "runs on 2.2.9" in their examples. The docs'
generated util.ts declarations pick up @ark/util 0.56.8.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

A tuple's
defaultablesbecomeoptionalson input andprefixelements on output, but thedefaultablesreduceIo assigned them overinner.optionals/inner.prefixinstead of combining them with what theprefixandoptionalschildren had already written (or would write next, depending on key order). Either the defaultable elements or the elements around them were lost:The inferred types (
T.inferIn,V.inferOut) were already right; only the runtime nodes were wrong.prefix,optionalsanddefaultablesnow each have areduceIothat combines with what's already there in tuple order (prefix, then defaultables on output; defaultables, then optionals on input), so the result doesn't depend on which key is reduced first. This follows the same pattern asrequired/optionalinstructure.ts.Related:
type(["number", "string = 'foo'"]).outcurrently throwsUnexpectedly encountered multiple distinct intersection results for refinement Type<>= 2>. That one also needs the intersection fix in #1697, because the output'sexactLengthof 2 has to be reduced against the originalminLength: 1/maxLength: 2. With both changes it gives[number, string]. I kept them separate since they're independent bugs.Tests
Added to
ark/type/__tests__/arrays/defaults.test.ts:[string, number = 5, boolean?]:.inand.outtypes, expressions andallows[number, string = 'foo', ...number[]]:.inand.outexpressionsBoth fail on
mainand pass with this change.What was run locally:
pnpm testFiles ark/type/__tests__/arrays/defaults.test.ts(with type assertions): 11 passingpnpm test: 1793 passingpnpm typecheckRepo: no errorsprettier --checkandeslinton the changed files: cleanNot run: the full
pnpm prChecks(buildRepo, benches,testV8,testIntegration,testTsVersions,testTypedLoose).