From ef9cd2cae414ab7a279ca874c46b2c5a9ff887da Mon Sep 17 00:00:00 2001 From: Ardit Tirana Date: Sun, 21 Jun 2026 03:07:03 +0200 Subject: [PATCH 1/2] `Get`: Distribute over unions so a key missing on some members yields `undefined` For a union base type, `Get` accessed the key on each member but a member lacking the key resolved to `unknown`, which absorbed the whole union (e.g. `Get<{a: number} | {b: string}, 'a'>` was `unknown` instead of `number | undefined`). `PropertyOf` now checks for a union first and, when distributing, treats a missing key as `undefined` for that member, while a missing key on a non-union type still resolves to `unknown` (unchanged, documented behaviour). Fixes #1205. --- source/get.d.ts | 28 ++++++++++++++++++++++++---- test-d/get.ts | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/source/get.d.ts b/source/get.d.ts index 554c4d6bd..6480b0e3c 100644 --- a/source/get.d.ts +++ b/source/get.d.ts @@ -4,6 +4,7 @@ import type {Paths} from './paths.d.ts'; import type {Split} from './split.d.ts'; import type {KeyAsString} from './key-as-string.d.ts'; import type {DigitCharacter} from './characters.d.ts'; +import type {IsUnion} from './is-union.d.ts'; export type GetOptions = { /** @@ -128,6 +129,25 @@ Note: - Returns `undefined` from nullish values, to match the behaviour of most deep-key libraries like `lodash`, `dot-prop`, etc. */ type PropertyOf> = + IsUnion extends true + // Distribute over the union so that a key which is missing on *some* + // members resolves to `undefined` for those members instead of + // collapsing the whole result to `unknown`. For example, + // `Get<{a: number} | {b: string}, 'a'>` is `number | undefined`. + ? BaseType extends unknown + ? SinglePropertyOf + : never + // Non-union: a missing key stays `unknown` (the documented behaviour, + // since structural typing can't guarantee the property is absent). + : SinglePropertyOf; + +/** +Get a property of a single (non-union) object or array. + +`MissingProperty` is the type returned when `Key` is not present on `BaseType` — +`unknown` for a plain lookup, or `undefined` when distributing over a union (see `PropertyOf`). +*/ +type SinglePropertyOf, MissingProperty> = BaseType extends null | undefined ? undefined : Key extends keyof BaseType @@ -142,9 +162,9 @@ type PropertyOf // Out-of-bounds access for tuples - : unknown + : MissingProperty // Non-numeric string key for arrays/tuples - : unknown + : MissingProperty // Handle array-like objects : BaseType extends { [n: number]: infer Item; @@ -153,11 +173,11 @@ type PropertyOf extends true ? Strictify - : unknown + : MissingProperty ) : Key extends keyof WithStringKeys ? StrictPropertyOf, Key, Options> - : unknown; + : MissingProperty; // This works by first splitting the path based on `.` and `[...]` characters into a tuple of string keys. Then it recursively uses the head key to get the next property of the current object, until there are no keys left. Number keys extract the item type from arrays, or are converted to strings to extract types from tuples and dictionaries with number keys. /** diff --git a/test-d/get.ts b/test-d/get.ts index 8b13a941d..a97a5f877 100644 --- a/test-d/get.ts +++ b/test-d/get.ts @@ -136,6 +136,24 @@ expectTypeOf>().toEqua expectTypeOf>().toEqualTypeOf(); expectTypeOf>().toEqualTypeOf(); +// Test union base types: a key missing on *some* members resolves to `undefined` +// for those members, instead of collapsing the whole result to `unknown`. +// https://github.com/sindresorhus/type-fest/issues/1205 +type DiscriminatedUnion = { + data: + | {type: 'number'; someValue: number} + | {type: 'string'; someValue: string} + | {type: 'none'}; +}; +// Key present on every member of the union. +expectTypeOf>().toEqualTypeOf<'number' | 'string' | 'none'>(); +// Key present on some members, absent on others. +expectTypeOf>().toEqualTypeOf(); +// Union as the base type directly. +expectTypeOf>().toEqualTypeOf(); +// A genuinely missing key on a non-union type still resolves to `unknown`. +expectTypeOf>().toEqualTypeOf(); + // Test empty path array expectTypeOf().toEqualTypeOf>(); expectTypeOf().toEqualTypeOf>(); From 73d01c5a2a531e910a196d4e64a24f55cc0f277f Mon Sep 17 00:00:00 2001 From: Ardit Tirana Date: Mon, 22 Jun 2026 16:04:02 +0200 Subject: [PATCH 2/2] `Get`: Resolve missing properties to `undefined` consistently Following review: a key that is not present on the base type now resolves to `undefined` rather than `unknown`, for both union and non-union base types. This matches the behaviour of deep-key libraries like lodash, and makes the result consistent with the union-distribution case (where a key missing on some members already became `undefined`). The `MissingProperty` parameter on `SinglePropertyOf` is no longer needed and is removed. Tests for out-of-bounds and missing-key access are updated from `unknown` to `undefined`. --- source/get.d.ts | 24 ++++++++++-------------- test-d/get.ts | 32 ++++++++++++++++---------------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/source/get.d.ts b/source/get.d.ts index 6480b0e3c..bcf1eaa54 100644 --- a/source/get.d.ts +++ b/source/get.d.ts @@ -125,29 +125,25 @@ type UncheckedIndex = [T] extends [Record = number`, and when indexing objects with number keys. Note: -- Returns `unknown` if `Key` is not a property of `BaseType`, since TypeScript uses structural typing, and it cannot be guaranteed that extra properties unknown to the type system will exist at runtime. -- Returns `undefined` from nullish values, to match the behaviour of most deep-key libraries like `lodash`, `dot-prop`, etc. +- Returns `undefined` if `Key` is not a property of `BaseType`, to match the behaviour of most deep-key libraries like `lodash`, `dot-prop`, etc. */ type PropertyOf> = IsUnion extends true // Distribute over the union so that a key which is missing on *some* // members resolves to `undefined` for those members instead of - // collapsing the whole result to `unknown`. For example, + // collapsing the whole result. For example, // `Get<{a: number} | {b: string}, 'a'>` is `number | undefined`. ? BaseType extends unknown - ? SinglePropertyOf + ? SinglePropertyOf : never - // Non-union: a missing key stays `unknown` (the documented behaviour, - // since structural typing can't guarantee the property is absent). - : SinglePropertyOf; + : SinglePropertyOf; /** Get a property of a single (non-union) object or array. -`MissingProperty` is the type returned when `Key` is not present on `BaseType` — -`unknown` for a plain lookup, or `undefined` when distributing over a union (see `PropertyOf`). +A `Key` that is not present on `BaseType` resolves to `undefined`. */ -type SinglePropertyOf, MissingProperty> = +type SinglePropertyOf> = BaseType extends null | undefined ? undefined : Key extends keyof BaseType @@ -162,9 +158,9 @@ type SinglePropertyOf // Out-of-bounds access for tuples - : MissingProperty + : undefined // Non-numeric string key for arrays/tuples - : MissingProperty + : undefined // Handle array-like objects : BaseType extends { [n: number]: infer Item; @@ -173,11 +169,11 @@ type SinglePropertyOf extends true ? Strictify - : MissingProperty + : undefined ) : Key extends keyof WithStringKeys ? StrictPropertyOf, Key, Options> - : MissingProperty; + : undefined; // This works by first splitting the path based on `.` and `[...]` characters into a tuple of string keys. Then it recursively uses the head key to get the next property of the current object, until there are no keys left. Number keys extract the item type from arrays, or are converted to strings to extract types from tuples and dictionaries with number keys. /** diff --git a/test-d/get.ts b/test-d/get.ts index a97a5f877..1452d7e8d 100644 --- a/test-d/get.ts +++ b/test-d/get.ts @@ -27,8 +27,8 @@ expectTypeOf(get(apiResponse, 'hits.hits.0._source.name')).toEqualTypeOf(); type WithDictionary = { foo: Record>().toBeNumber(); expectTypeOf>().toBeNumber(); expectTypeOf>().toBeBoolean(); -expectTypeOf>().toBeUnknown(); +expectTypeOf>().toEqualTypeOf(); -expectTypeOf>().toBeUnknown(); -expectTypeOf>().toBeUnknown(); +expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); type EmptyTuple = Parameters<() => {}>; -expectTypeOf>().toBeUnknown(); -expectTypeOf>().toBeUnknown(); -expectTypeOf>().toBeUnknown(); +expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); expectTypeOf>().toEqualTypeOf<0>(); type WithNumberKeys = { @@ -87,8 +87,8 @@ type WithNumberKeys = { expectTypeOf>().toBeNumber(); expectTypeOf>().toBeNumber(); -expectTypeOf>().toBeUnknown(); -expectTypeOf>().toBeUnknown(); +expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); // Test `readonly`, `ReadonlyArray`, optional properties, and unions with null. @@ -112,9 +112,9 @@ expectTypeOf>().toEqualTypeO // Test bracket notation expectTypeOf>().toBeNumber(); // NOTE: This would fail if `[0][0]` was converted into `00`: -expectTypeOf>().toBeUnknown(); +expectTypeOf>().toEqualTypeOf(); expectTypeOf>().toBeNumber(); -expectTypeOf>().toBeUnknown(); +expectTypeOf>().toEqualTypeOf(); expectTypeOf>>}}, 'a.b[0][0][0].id', NonStrict>>().toBeNumber(); expectTypeOf>>}}, ['a', 'b', '0', '0', '0', 'id'], NonStrict>>().toBeNumber(); @@ -133,8 +133,8 @@ expectTypeOf>().toEqualTypeOf>().toEqualTypeOf(); // Test array index out of bounds -expectTypeOf>().toEqualTypeOf(); -expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); // Test union base types: a key missing on *some* members resolves to `undefined` // for those members, instead of collapsing the whole result to `unknown`. @@ -151,8 +151,8 @@ expectTypeOf>().toEqualTypeOf<'number' | 's expectTypeOf>().toEqualTypeOf(); // Union as the base type directly. expectTypeOf>().toEqualTypeOf(); -// A genuinely missing key on a non-union type still resolves to `unknown`. -expectTypeOf>().toEqualTypeOf(); +// A genuinely missing key on a non-union type resolves to `undefined`, consistent with the union case. +expectTypeOf>().toEqualTypeOf(); // Test empty path array expectTypeOf().toEqualTypeOf>();