From 8a52c78d5af1b5d2747684bf4259344531f26444 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Wed, 30 Apr 2025 14:36:58 +0530 Subject: [PATCH 1/2] feat: add set-non-nullable-deep type --- index.d.ts | 1 + readme.md | 1 + source/set-non-nullable-deep.d.ts | 83 ++++++++++++++++ test-d/set-non-nullable-deep.ts | 158 ++++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+) create mode 100644 source/set-non-nullable-deep.d.ts create mode 100644 test-d/set-non-nullable-deep.ts diff --git a/index.d.ts b/index.d.ts index f03837c6a..979e50466 100644 --- a/index.d.ts +++ b/index.d.ts @@ -47,6 +47,7 @@ export type {SetReadonly} from './source/set-readonly'; export type {SetRequired} from './source/set-required'; export type {SetRequiredDeep} from './source/set-required-deep'; export type {SetNonNullable} from './source/set-non-nullable'; +export type {SetNonNullableDeep} from './source/set-non-nullable-deep'; export type {ValueOf} from './source/value-of'; export type {AsyncReturnType} from './source/async-return-type'; export type {ConditionalExcept} from './source/conditional-except'; diff --git a/readme.md b/readme.md index baa648a24..d73ac3dc6 100644 --- a/readme.md +++ b/readme.md @@ -151,6 +151,7 @@ Click the type names for complete docs. - [`SetRequired`](source/set-required.d.ts) - Create a type that makes the given keys required. - [`SetRequiredDeep`](source/set-required-deep.d.ts) - Like `SetRequired` except it selects the keys deeply. - [`SetNonNullable`](source/set-non-nullable.d.ts) - Create a type that makes the given keys non-nullable. +- [`SetNonNullableDeep`](source/set-non-nullable-deep.d.ts) - Create a type that makes the specified keys non-nullable (removes `null` and `undefined`), supports deeply nested key paths, and leaves all other keys unchanged. - [`ValueOf`](source/value-of.d.ts) - Create a union of the given object's values, and optionally specify which keys to get the values from. - [`ConditionalKeys`](source/conditional-keys.d.ts) - Extract keys from a shape where values extend the given `Condition` type. - [`ConditionalPick`](source/conditional-pick.d.ts) - Like `Pick` except it selects properties from a shape where the values extend the given `Condition` type. diff --git a/source/set-non-nullable-deep.d.ts b/source/set-non-nullable-deep.d.ts new file mode 100644 index 000000000..9864db4d6 --- /dev/null +++ b/source/set-non-nullable-deep.d.ts @@ -0,0 +1,83 @@ +import type {NonRecursiveType, StringToNumber} from './internal'; +import type {Paths} from './paths'; +import type {SetNonNullable} from './set-non-nullable'; +import type {Simplify} from './simplify'; +import type {UnionToTuple} from './union-to-tuple'; +import type {UnknownArray} from './unknown-array'; + +/** +Create a type that makes the specified keys non-nullable (removes `null` and `undefined`), supports deeply nested key paths, and leaves all other keys unchanged. + +NOTE: Optional modifiers (`?`) are not removed from properties, for e.g., `SetNonNullableDeep<{foo?: string | null | undefined}, 'foo'>` will result in `{foo?: string}`. + +@example +``` +import type {SetNonNullableDeep} from 'type-fest'; + +type User = { + name: string; + address: { + city: string | undefined; + street?: string | null; + }; + contact: { + email?: string | null | undefined; + phone: string | undefined; + }; +}; + +type UpdatedUser = SetNonNullableDeep; +//=> { +// name: string; +// address: { +// city: string | undefined; +// street?: string; +// }; +// contact: { +// email?: string; +// phone: string; +// }; +// }; +``` + +@example +``` +import type {SetNonNullableDeep} from 'type-fest'; + +// Set specific indices in an array to be non-nullable. +type ArrayExample1 = SetNonNullableDeep<{a: [number | null, number | null, number | undefined]}, 'a.1' | 'a.2'>; +//=> {a: [number | null, number, number]} + +// Optional modifier (`?`) is not removed. +type ArrayExample2 = SetNonNullableDeep<{a: [(number | null)?, (number | null)?]}, 'a.1'>; +//=> {a: [(number | null)?, number?]} +``` + +@category Object +*/ +export type SetNonNullableDeep> = + SetNonNullableDeepHelper>; + +/** +Internal helper for {@link SetNonNullableDeep}. + +Recursively transforms the `BaseType` by applying {@link SetNonNullableDeepSinglePath} for each path in `KeyPathsTuple`. +*/ +type SetNonNullableDeepHelper = + KeyPathsTuple extends [infer KeyPath, ...infer RestPaths] + ? SetNonNullableDeepHelper, RestPaths> + : BaseType; + +/** +Makes a single path non-nullable in `BaseType`. +*/ +type SetNonNullableDeepSinglePath = + BaseType extends NonRecursiveType | ReadonlySet | ReadonlyMap // Also distributes `BaseType` + ? BaseType + : KeyPath extends `${infer Property}.${infer RestPath}` + ? { + [Key in keyof BaseType]: Property extends `${Key & (string | number)}` + ? SetNonNullableDeepSinglePath + : BaseType[Key]; + } + : Simplify) & keyof BaseType>>; diff --git a/test-d/set-non-nullable-deep.ts b/test-d/set-non-nullable-deep.ts new file mode 100644 index 000000000..4eb6c9aab --- /dev/null +++ b/test-d/set-non-nullable-deep.ts @@ -0,0 +1,158 @@ +import {expectType} from 'tsd'; +import type {SetNonNullableDeep} from '../source/set-non-nullable-deep'; + +expectType<{a: number}>({} as SetNonNullableDeep<{a: number | null}, 'a'>); +expectType<{a: number}>({} as SetNonNullableDeep<{a: number | undefined}, 'a'>); +expectType<{a: number}>({} as SetNonNullableDeep<{a: number | null | undefined}, 'a'>); +expectType<{a?: number}>({} as SetNonNullableDeep<{a?: number | null}, 'a'>); +expectType<{a?: number}>({} as SetNonNullableDeep<{a?: number | undefined}, 'a'>); +expectType<{a?: number}>({} as SetNonNullableDeep<{a?: number | null | undefined}, 'a'>); + +expectType<{a: number; b: {c: string}}>({} as SetNonNullableDeep<{a: number; b: {c: string | null}}, 'b.c'>); +expectType<{a: number; b: {c: string}}>({} as SetNonNullableDeep<{a: number; b: {c: string | undefined}}, 'b.c'>); +expectType<{a: number; b: {c: string}}>({} as SetNonNullableDeep<{a: number; b: {c: string | null | undefined}}, 'b.c'>); +expectType<{a: number; b: {c?: string}}>({} as SetNonNullableDeep<{a: number; b: {c?: string | null}}, 'b.c'>); +expectType<{a: number; b: {c?: string}}>({} as SetNonNullableDeep<{a: number; b: {c?: string | undefined}}, 'b.c'>); +expectType<{a: number; b: {c?: string}}>({} as SetNonNullableDeep<{a: number; b: {c?: string | null | undefined}}, 'b.c'>); + +// Becomes `never` when the value is only `null` or `undefined` +expectType<{a: never}>({} as SetNonNullableDeep<{a: null | undefined}, 'a'>); +expectType<{a?: never}>({} as SetNonNullableDeep<{a?: null | undefined}, 'a'>); + +// Ignores keys that are already non-nullable +expectType<{a: number}>({} as SetNonNullableDeep<{a: number}, 'a'>); +expectType<{readonly a?: number}>({} as SetNonNullableDeep<{readonly a?: number}, 'a'>); +expectType<{a?: number; b: {c: string}}>({} as SetNonNullableDeep<{a?: number; b: {c: string}}, 'b.c'>); +expectType<{a: number; readonly b?: {c?: string}}>({} as SetNonNullableDeep<{a: number; readonly b?: {c?: string}}, 'b.c'>); + +// Removes nullables only from the specified keys, nullables in other paths are preserved +expectType<{a: number; b: {c: string | null | undefined} | null | undefined}>( + {} as SetNonNullableDeep<{a: number | null | undefined; b: {c: string | null | undefined} | null | undefined}, 'a'>, +); +expectType<{a: number | null | undefined; b: {c: string | null | undefined}}>( + {} as SetNonNullableDeep<{a: number | null | undefined; b: {c: string | null | undefined} | null | undefined}, 'b'>, +); +expectType<{a: number | null | undefined; b: {c: string} | null | undefined}>( + {} as SetNonNullableDeep<{a: number | null | undefined; b: {c: string | null | undefined} | null | undefined}, 'b.c'>, +); +expectType<{a: {b: string}}>({} as SetNonNullableDeep<{a: {b: string | null} | null | undefined}, 'a' | 'a.b'>); +expectType<{a: {b: {c: string}} | null | undefined}>({} as SetNonNullableDeep<{a: {b: {c: string | undefined} | null} | null | undefined}, 'a.b' | 'a.b.c'>); +expectType<{a: {b: {c: string} | null}}>({} as SetNonNullableDeep<{a: {b: {c: string | undefined} | null} | null | undefined}, 'a' | 'a.b.c'>); +expectType<{a: {b?: {c: {d?: {e: number | string}; f: {g: boolean}} | undefined} | null; h: number | null}}>( + {} as SetNonNullableDeep< + {a: {b?: {c: {d?: {e: number | string} | null; f: {g: boolean | null}} | undefined} | null; h: number | null} | undefined}, + 'a' | 'a.b.c.d' | 'a.b.c.f.g' + >, +); + +// Unions +expectType<{a: string} | {a?: number} | {a: boolean}>({} as SetNonNullableDeep<{a: string | null} | {a?: number | undefined} | {a: boolean | null | undefined}, 'a'>); +expectType<{a: string; b: number}>({} as SetNonNullableDeep<{a: string | null; b: number | undefined}, 'a' | 'b'>); +expectType<{a: string; b: {c: {d: {e: number}} | null} | undefined}>( + {} as SetNonNullableDeep<{a: string | null; b: {c: {d: {e: number | undefined}} | null} | undefined}, 'a' | 'b.c.d.e'>, +); +expectType<{a: {b: string | null}} | {a?: number; b: {c?: string}}>( + {} as SetNonNullableDeep<{a: {b: string | null} | undefined} | {a?: number | undefined; b: {c?: string}}, 'a' | 'b.c'>, +); +expectType<{a: string; b: {c?: {d: string} | undefined; f?: number}} | {a: {b: number}; c: never; d?: undefined}>( + {} as SetNonNullableDeep< + {a: string; b: {c?: {d: string | null} | undefined; f?: number} | null} | {a: {b: number | null}; c: null; d?: undefined}, + 'b' | 'b.c.d' | 'a.b' | 'c' + >, +); +expectType<{a: 1; b: {c: 2}; d?: {e?: {f?: 2}; g?: 3}}>( + {} as SetNonNullableDeep<{a: 1 | null; b: {c: 2 | null}; d?: {e?: {f?: 2 | null}; g?: 3}}, 'a' | 'b' | 'b.c' | 'd.e.f' | 'd.g'>, +); +expectType<{a: {b: string} | {c: string} | {b: {c: string | null}}}>( + {} as SetNonNullableDeep<{a: {b: string | null} | {c: string} | {b: {c: string | null} | undefined}}, 'a.b'>, +); + +// Preserves non-nullable values when they are in a union with objects +expectType<{a?: {b: string} | number}>({} as SetNonNullableDeep<{a?: {b: string} | number | null | undefined}, 'a'>); +expectType<{a: {b: Array<{c?: number | null}> | number}}>({} as SetNonNullableDeep<{a: {b: Array<{c?: number | null}> | number | null}}, 'a.b'>); +expectType<{a: {b: string | number} | number}>({} as SetNonNullableDeep<{a: {b: string | number | null} | number | null | undefined}, 'a' | 'a.b'>); +expectType<{a: number; b: {c: number | null} | {d: string}}>({} as SetNonNullableDeep<{a: number; b: {c: number | null} | {d: string | undefined} | null}, 'b' | 'b.d'>); + +// Preserves `readonly` modifier +expectType<{a: string; b: {readonly c: number}}>({} as SetNonNullableDeep<{a: string; b: {readonly c: number | null}}, 'b.c'>); +expectType<{readonly a: string; readonly b: {c: number | null}}>({} as SetNonNullableDeep<{readonly a: string | null; readonly b: {c: number | null} | null}, 'a' | 'b'>); +expectType<{readonly a: string; readonly b: {readonly c: number}}>({} as SetNonNullableDeep<{readonly a: string | null; readonly b: {readonly c: number | null} | null}, 'a' | 'b' | 'b.c'>); + +// Number keys +expectType<{0: 1; 1: {2?: string}}>({} as SetNonNullableDeep<{0: 1; 1: {2?: string | null}}, '1.2'>); +expectType<{0: 1; 1?: {2: string | null}}>({} as SetNonNullableDeep<{0: 1 | null; 1?: {2: string | null} | undefined}, 0 | 1>); + +// Number keys containing dots +// NOTE: Passing "1.2" instead of 1.2 will treat it as a path instead of a key +expectType<{1.2?: string; 1?: {2?: string | null} | null}>({} as SetNonNullableDeep<{1.2?: string | null; 1?: {2?: string | null} | null}, 1.2>); +expectType<{1.2?: string | null; 1?: {2?: string} | null}>({} as SetNonNullableDeep<{1.2?: string | null; 1?: {2?: string | null} | null}, '1.2'>); +expectType<{1.2?: string; 1?: {2?: string} | undefined}>({} as SetNonNullableDeep<{1.2?: string | undefined; 1?: {2?: string | undefined} | undefined}, 1.2 | '1.2'>); + +// Index signatures +expectType<{[x: string]: any; a: number; b: {c: number}}>({} as SetNonNullableDeep<{[x: string]: any; a: number | null; b: {c: number | null}}, 'a' | 'b.c'>); + +// Works with `KeyPaths` containing template literals +expectType<{a: number | null; b: {c: number} | {d: number} | null | undefined}>({} as SetNonNullableDeep<{a: number | null; b: {c: number | null} | {d: number | undefined} | null | undefined}, `b.${'c' | 'd'}`>); +expectType<{a: number; b: null | {readonly c: {1: number[]} | undefined} | {d: {1: number[]} | null}}>({} as SetNonNullableDeep< +{a: number | undefined; b: null | {readonly c: {1: number[] | undefined} | undefined} | {d: {1: number[] | undefined} | null}}, 'a' | `b.${'c' | 'd'}.1` +>); + +// Non recursive types +expectType<{a: {b: never} | Set}>({} as SetNonNullableDeep<{a: {b: null} | Set}, 'a.b'>); +expectType<{a: {b: {c: string} | Map}}>({} as SetNonNullableDeep<{a: {b: {c: string} | Map | null}}, 'a.b'>); + +// === Arrays === +expectType<[string, number | null, boolean]>({} as SetNonNullableDeep<[string | null, number | null, boolean | undefined], '0' | '2'>); +expectType<{a?: [string, number, (boolean | null)?] | null}>({} as SetNonNullableDeep<{a?: [string | undefined, number | undefined, (boolean | null)?] | null}, 'a.0' | 'a.1'>); +expectType<{a?: [string | null, number?, boolean?] | null}>({} as SetNonNullableDeep<{a?: [string | null, (number | undefined)?, (boolean | null)?] | null}, 'a.1' | 'a.2'>); +expectType<{a: readonly [string, number, (boolean | null)?]}>({} as SetNonNullableDeep<{a: readonly [string, number | undefined, (boolean | null)?]}, 'a.1'>); +expectType<{readonly a: [string, number | null | undefined, boolean, ...Array]}>( + {} as SetNonNullableDeep<{readonly a: [string | null, number | null | undefined, boolean | undefined, ...Array]}, 'a.0' | 'a.2'>, +); +expectType<{readonly a?: [string, number, boolean, ...Array]}>( + {} as SetNonNullableDeep<{readonly a?: [string, number | null, boolean | undefined, ...Array] | undefined}, 'a' | 'a.1' | 'a.2'>, +); + +// Readonly arrays +expectType<{a?: {b?: readonly [(string | number)?]} | null}>({} as SetNonNullableDeep<{a?: {b?: readonly [(string | number | null)?]} | null}, 'a.b.0'>); +expectType<{a: readonly [string, number, boolean, ...Array] | undefined}>( + {} as SetNonNullableDeep<{a: readonly [string | null, number | undefined, boolean | null, ...Array] | undefined}, 'a.0' | 'a.1' | 'a.2'>, +); + +// Ignores `Keys` that are already non-nullable +expectType<{a: [string, (number | null)?, boolean?]}>({} as SetNonNullableDeep<{a: [string, (number | null)?, boolean?]}, 'a.0'>); +expectType<{a: [string, number?, boolean?]}>({} as SetNonNullableDeep<{a: [string, (number | null)?, boolean?]}, 'a.0' | 'a.1'>); + +// Ignores `Keys` that are not known +// This case is only possible when the array contains a rest element, +// because otherwise the constaint on `KeyPaths` would disallow out of bound keys. +expectType<{a?: readonly [string | null, (number | undefined)?, (boolean | null)?, ...Array] | undefined}>( + {} as SetNonNullableDeep<{a?: readonly [string | null, (number | undefined)?, (boolean | null)?, ...Array] | undefined}, 'a.10'>, +); + +// Unions of arrays +expectType<{a: [string] | [string, number?, (boolean | null)?, ...Array] | readonly [string, number, (boolean | undefined)?]}>( + {} as SetNonNullableDeep<{a: [string | undefined] | [string | null, number?, (boolean | null)?, ...Array] | readonly [string | null | undefined, number | null, (boolean | undefined)?]}, 'a.0' | 'a.1'>, +); + +// Labelled tuples +expectType<{a?: [b: string, c: number] | undefined}>({} as SetNonNullableDeep<{a?: [b: string | null, c: number | undefined] | undefined}, 'a.0' | 'a.1'>); + +// Non-tuple arrays +expectType<{a: string[]}>({} as SetNonNullableDeep<{a: Array}, `a.${number}`>); +expectType<{readonly a: ReadonlyArray}>({} as SetNonNullableDeep<{readonly a: ReadonlyArray | undefined}, 'a' | `a.${number}`>); + +// Nested arrays +expectType<{a?: [([string?, (number | null)?] | null)?]}>({} as SetNonNullableDeep<{a?: [([(string | undefined)?, (number | null)?] | null)?] | undefined}, 'a' | 'a.0.0'>); +expectType<{a?: [[(string | undefined)?, number?]?] | undefined}>({} as SetNonNullableDeep<{a?: [([(string | undefined)?, (number | null)?] | null)?] | undefined}, 'a.0.1' | 'a.0'>); +expectType<{a?: [[string | null, number]?] | null}>({} as SetNonNullableDeep<{a?: [([string | null, number | undefined] | null)?] | null}, 'a.0' | 'a.0.1'>); +expectType<{a?: Array<[string | null, number?]> | null}>({} as SetNonNullableDeep<{a?: Array<[string | null, (number | undefined)?]> | null}, `a.${number}.1`>); + +// Removes `null` & `undefined` from keys inside arrays +expectType<{a?: Array<{b: number}> | undefined}>({} as SetNonNullableDeep<{a?: Array<{b: number | null}> | undefined}, `a.${number}.b`>); +expectType<{readonly a?: [{readonly b: number}]}>({} as SetNonNullableDeep<{readonly a?: [{readonly b: number | undefined}] | null}, 'a' | 'a.0' | 'a.0.b'>); +expectType<{readonly a: [{readonly b: number}, {c?: string | null}?]}>( + {} as SetNonNullableDeep<{readonly a: [{readonly b: number | null | undefined}, ({c?: string | null} | undefined)?]}, 'a.0.b' | 'a.1' >, +); +expectType<{a?: Array<{b: number; c?: string | null}> | null}>({} as SetNonNullableDeep<{a?: Array<{b: number | undefined; c?: string | null}> | null}, `a.${number}.b`>); +expectType<{a: [{b?: number | null; readonly c: string}]}>({} as SetNonNullableDeep<{a: [{b?: number | null; readonly c: string | undefined}]}, 'a.0.c'>); From 36752950f502e3e91e59b433c4844edec2d55ac7 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 6 May 2025 14:13:56 +0700 Subject: [PATCH 2/2] Update set-non-nullable-deep.d.ts --- source/set-non-nullable-deep.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/set-non-nullable-deep.d.ts b/source/set-non-nullable-deep.d.ts index 9864db4d6..ddf78f2dc 100644 --- a/source/set-non-nullable-deep.d.ts +++ b/source/set-non-nullable-deep.d.ts @@ -8,7 +8,7 @@ import type {UnknownArray} from './unknown-array'; /** Create a type that makes the specified keys non-nullable (removes `null` and `undefined`), supports deeply nested key paths, and leaves all other keys unchanged. -NOTE: Optional modifiers (`?`) are not removed from properties, for e.g., `SetNonNullableDeep<{foo?: string | null | undefined}, 'foo'>` will result in `{foo?: string}`. +NOTE: Optional modifiers (`?`) are not removed from properties. For example, `SetNonNullableDeep<{foo?: string | null | undefined}, 'foo'>` will result in `{foo?: string}`. @example ```