Skip to content

fix(schema): keep object checks for an undefined case on a nested path - #1725

Open
RClaudiuM wants to merge 1 commit into
arktypeio:mainfrom
RClaudiuM:fix/1724-undefined-discriminant-primitive
Open

RClaudiuM wants to merge 1 commit into
arktypeio:mainfrom
RClaudiuM:fix/1724-undefined-discriminant-primitive

Conversation

@RClaudiuM

Copy link
Copy Markdown

Fixes #1724

What

Unions discriminated on a nested path no longer drop the object checks from the branch matched by an undefined case:

const Thing = type({ k: "1", a: "2" }).or({ k: "3", a: "undefined" })
Thing.allows(0) // false (was TypeError: Cannot use 'in' operator to search for 'k' in 0)
Thing(0).toString() // "must be an object (was a number)"

const T = type({ a: "2" }).or({ a: "undefined" })
T.allows(0) // false (was true)
T.allows({}) // false (was true)
T.allows({ a: undefined }) // true

Why

A discriminant on a nested path compiles to switch (data?.a) (or typeof data?.a for a domain discriminant). For every other case, matching it means data is an object with an a key, so pruneDiscriminant removes the branch's domain: "object" and its required-key check. data?.a is also undefined when data is a primitive, null or undefined, or when the key is missing, so for the undefined case those checks are still needed. Without them the required check ran "k" in 0 and threw, and a branch with only the discriminant key compiled to return true, accepting any primitive or {}.

resolveCase now leaves the branches (including overlapping ones) unpruned when the case is undefined, as a unit or a domain, and the path isn't empty. Root-level undefined cases are exact, so they're still pruned. The jitless path didn't have this bug and is unchanged.

Tests

In ark/type/__tests__/discrimination.test.ts, each with JIT and jitless: true where it applies:

  • the example from the issue rejects 0, "s", null and undefined with "must be an object", and still checks k
  • { a: 2 } | { a: undefined } rejects 0 and {} and accepts { a: undefined }, also for a two-level path ({ a: { b: ... } })
  • a domain discriminant with an "undefined" case ({ a: string } | { a: undefined } | { a: number }) keeps domain: "object" and the a key in that case

The three tests fail on main (the first throws the TypeError from the issue) and pass with the change. No existing snapshot changed.

Checks run locally (Node 24): pnpm lint, pnpm typecheckRepo, pnpm test (1816 passing), pnpm testFiles ark/type/__tests__/discrimination.test.ts with type checking, pnpm build, pnpm testV8, pnpm testIntegration and the @ark/attest tests. I didn't run the benches or testTsVersions from prChecks.

  • Code is up-to-date with the main branch (based on 326cdce)
  • There are new or updated unit tests validating the change

A union discriminated on a nested path compiles to a switch on
`data?.a`. That value is also `undefined` when `data` isn't an object
or has no `a` key, so the `undefined` case can't drop the branch's
object domain or its required key the way other cases do. Pruning them
made `type({ k: "1", a: "2" }).or({ k: "3", a: "undefined" })` throw on
primitive input, and `type({ a: "2" }).or({ a: "undefined" })` accept
`0` and `{}`.

Keep branches resolved for an `undefined` case on a nested path as they
are.

Fixes arktypeio#1724
@github-project-automation github-project-automation Bot moved this to To do in arktypeio Nov 7, 2026
ssalbdivad added a commit that referenced this pull request Nov 9, 2026
…uthors

Each fix below landed on featherduster from a port or an independent fix,
but each started from a reasonable community PR against 2.x, and the
CHANGELOG thanks every contributor whose work this release supersedes:

- #1721 (@RaphaelFakhri): fast-check keeps a regex's flags (#1720)
- #1722 (@louisss1016): Temporal values survive a morph / deepClone (#1719)
- #1725 (@RClaudiuM): an undefined discriminant case checks its key (#1724)
- #1715 (@SynthLuvr): ~standard.jsonSchema.input drops undefined (#1462);
  only its input half was taken, output keeps routing to fallback.unit

#1718 is not credited: 3.0 answers #1469 with jsonSchema meta rather than
its bare pattern key. #1705 and #1695 were already thanked.

Also notes that a cyclic index signature with no way out now generates
shrinking dictionaries instead of overflowing the stack (6f4dd5d).
@ssalbdivad ssalbdivad mentioned this pull request Nov 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To do

Development

Successfully merging this pull request may close these issues.

Union discriminated on an "undefined" value throws TypeError for non-object input

1 participant