Skip to content

fix(schema): keep tuple elements around defaults in .in and .out - #1696

Merged
ssalbdivad merged 2 commits into
arktypeio:mainfrom
ybai08:fix/tuple-default-io
Oct 20, 2026
Merged

ssalbdivad merged 2 commits into
arktypeio:mainfrom
ybai08:fix/tuple-default-io

Conversation

@ybai08

@ybai08 ybai08 commented Oct 17, 2026 •

Copy link
Copy Markdown

A tuple's defaultables become optionals on input and prefix elements on output, but the defaultables reduceIo assigned them over inner.optionals / inner.prefix instead of combining them with what the prefix and optionals children had already written (or would write next, depending on key order). Either the defaultable elements or the elements around them were lost:

const T = type(["string", "number = 5", "boolean?"])
T.in.expression // "[string, boolean?]", so T.in.allows(["", true]) is true
// expected "[string, number?, boolean?]"

const V = type(["number", "string = 'foo'", "...", "number[]"])
V.out.expression // "number[]"
// expected "[number, string, ...number[]]"

The inferred types (T.inferIn, V.inferOut) were already right; only the runtime nodes were wrong.

prefix, optionals and defaultables now each have a reduceIo that 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 as required/optional in structure.ts.

Related: type(["number", "string = 'foo'"]).out currently throws Unexpectedly encountered multiple distinct intersection results for refinement Type<>= 2>. That one also needs the intersection fix in #1697, because the output's exactLength of 2 has to be reduced against the original minLength: 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?]: .in and .out types, expressions and allows
  • [number, string = 'foo', ...number[]]: .in and .out expressions

Both fail on main and pass with this change.

What was run locally:

  • pnpm testFiles ark/type/__tests__/arrays/defaults.test.ts (with type assertions): 11 passing
  • pnpm test: 1793 passing
  • pnpm typecheckRepo: no errors
  • prettier --check and eslint on the changed files: clean

Not run: the full pnpm prChecks (buildRepo, benches, testV8, testIntegration, testTsVersions, testTypedLoose).

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[].

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — verified the tuple reduceIo composition fix and its regression tests against main (c34bf69).

  • Tuple reduceIo composes instead of overwriting — prefix, optionals, and defaultables now merge into inner.prefix/inner.optionals in 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: prefix prepends, defaultables prepends on in/appends on out, and optionals appends, so any key reduction order yields the same result.
  • Regression tests — [string, number = 5, boolean?] and [number, string = 'foo', ...number[]] assert .in/.out expressions, inferred types, and allows.

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).

Pullfrog  | View workflow run | Using 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
ssalbdivad merged commit 092075a into arktypeio:main Oct 20, 2026
13 checks passed
@github-project-automation github-project-automation Bot moved this from To do to Done (merged or closed) in arktypeio Oct 20, 2026
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>
@ssalbdivad

Copy link
Copy Markdown
Member

thanks @ybai08! merged this together with your #1697 since they depend on each other. also trimmed the merges inside defaultables, since keys always reduce in sorted order so those never ran

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done (merged or closed)

Development

Successfully merging this pull request may close these issues.

2 participants