diff --git a/source/get.d.ts b/source/get.d.ts index 554c4d6bd..bcf1eaa54 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 = { /** @@ -124,10 +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. For example, + // `Get<{a: number} | {b: string}, 'a'>` is `number | undefined`. + ? BaseType extends unknown + ? SinglePropertyOf + : never + : SinglePropertyOf; + +/** +Get a property of a single (non-union) object or array. + +A `Key` that is not present on `BaseType` resolves to `undefined`. +*/ +type SinglePropertyOf> = BaseType extends null | undefined ? undefined : Key extends keyof BaseType @@ -142,9 +158,9 @@ type PropertyOf // Out-of-bounds access for tuples - : unknown + : undefined // Non-numeric string key for arrays/tuples - : unknown + : undefined // Handle array-like objects : BaseType extends { [n: number]: infer Item; @@ -153,11 +169,11 @@ type PropertyOf extends true ? Strictify - : unknown + : undefined ) : Key extends keyof WithStringKeys ? StrictPropertyOf, Key, Options> - : unknown; + : 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 8b13a941d..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,26 @@ 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`. +// 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 resolves to `undefined`, consistent with the union case. +expectTypeOf>().toEqualTypeOf(); // Test empty path array expectTypeOf().toEqualTypeOf>();