fix: support union types in Get (#1499) - #1517
katariyaVivek wants to merge 2 commits into
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
I don't think we should change the default behavior. Declaring The |
|
Thanks for the insightful review and explanation @sindresorhus — that makes total sense. I verified the runtime behavior you mentioned: because TypeScript is structurally typed, an object satisfying an omitted-property branch can indeed hold an unexpected value at runtime, so assuming I've reverted the type change to preserve the existing default behavior and updated this PR to:
Let me know if this documentation and test update looks good to you! |
Problem
When using
Get<BaseType, Path>whereBaseTypeis a union of objects, if an intermediate or leaf property inPathis declared on only some union constituents (but not all),Getimproperly collapses tounknownrather than distributing across the union and returningResolvedType | undefined.For example:
This caused a type safety hole: because
Getreturnedunknown, arbitrary values (e.g.123) were assignable toResult.Root Cause
In
source/get.d.ts,PropertyOfevaluatesKey extends keyof BaseType. In TypeScript,keyof (A | B)produces the intersection of keys ((keyof A) & (keyof B)). WhenBaseTypeis a union where some members do not declareKey,Key extends keyof BaseTypeevaluates tofalsefor the union. Additionally, whenBaseTypedistributes overBaseType extends null | undefined, each individual constituent that lacksKeyfell through the array/record checks down to the finalunknownfallback. Becauseunknown | T === unknownin TypeScript, returningunknownon the missing constituent absorbed the valid type from the matching constituent, collapsing the entire union tounknown.Solution
KeysOfUnion<ObjectType>to compute the union of keys across all constituents:PropertyOf, capturedAllKeys = KeysOfUnion<BaseType>as a default type parameter before distributive conditional evaluation.Keyis not on the specific constituent being evaluated, checkedKey extends AllKeys ? undefined : unknown. BecauseKeyis a known property of the union hierarchy, the missing constituent returnsundefined, which unions cleanly with the present constituent (string | undefined), while completely unknown properties continue to fall through tounknown.Testing
Added comprehensive test cases in
test-d/get.ts:Get<>==>unknown#1499:Get<UnionTest, 'foo.bar_only_in_test2', NonStrict>(fails pre-fix asunknown, passes post-fix asstring | undefined).Get<UnionTest, 'foo.bar_only_in_test2'>->string | undefined.Get<UnionTest, 'foo.bar', NonStrict>->number | boolean.Get<UnionTest, 'foo.doesNotExist', NonStrict>->unknown.Get<DeepUnion, 'a.b.c'>->string | undefined.Get<DeepUnion, 'a.b.shared'>->boolean.Get<IntermediateDivergence, 'profile.name'>andGet<IntermediateDivergence, 'details.id'>->string | undefined.Verification
node --max-old-space-size=6144 ./node_modules/typescript/bin/tsc --project tsconfig.json(0 errors across entire repo).npx xo source/get.d.ts test-d/get.ts(0 errors, 0 warnings).Impact
Get<BaseType, Path>now correctly resolves properties on union types without collapsing tounknown. Single objects, tuples, arrays, and non-existent properties are completely unaffected.Compatibility
Backwards compatible. Fixes #1499.