Skip to content

Commit 7a21325

Browse files
committed
feat: add set-non-nullable-deep type
1 parent afd809a commit 7a21325

File tree

4 files changed

+242
-0
lines changed

4 files changed

+242
-0
lines changed

index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export type {SetReadonly} from './source/set-readonly';
4747
export type {SetRequired} from './source/set-required';
4848
export type {SetRequiredDeep} from './source/set-required-deep';
4949
export type {SetNonNullable} from './source/set-non-nullable';
50+
export type {SetNonNullableDeep} from './source/set-non-nullable-deep';
5051
export type {ValueOf} from './source/value-of';
5152
export type {AsyncReturnType} from './source/async-return-type';
5253
export type {ConditionalExcept} from './source/conditional-except';

readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ Click the type names for complete docs.
151151
- [`SetRequired`](source/set-required.d.ts) - Create a type that makes the given keys required.
152152
- [`SetRequiredDeep`](source/set-required-deep.d.ts) - Like `SetRequired` except it selects the keys deeply.
153153
- [`SetNonNullable`](source/set-non-nullable.d.ts) - Create a type that makes the given keys non-nullable.
154+
- [`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.
154155
- [`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.
155156
- [`ConditionalKeys`](source/conditional-keys.d.ts) - Extract keys from a shape where values extend the given `Condition` type.
156157
- [`ConditionalPick`](source/conditional-pick.d.ts) - Like `Pick` except it selects properties from a shape where the values extend the given `Condition` type.

source/set-non-nullable-deep.d.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type {NonRecursiveType, StringToNumber} from './internal';
2+
import type {Paths} from './paths';
3+
import type {SetNonNullable} from './set-non-nullable';
4+
import type {Simplify} from './simplify';
5+
import type {UnionToTuple} from './union-to-tuple';
6+
import type {UnknownArray} from './unknown-array';
7+
8+
/**
9+
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.
10+
11+
NOTE: Optional modifiers (`?`) are not removed from properties, for e.g., `SetNonNullableDeep<{foo?: string | null | undefined}, 'foo'>` will result in `{foo?: string}`.
12+
13+
@example
14+
```
15+
import type {SetNonNullableDeep} from 'type-fest';
16+
17+
type User = {
18+
name: string;
19+
address: {
20+
city: string | undefined;
21+
street?: string | null;
22+
};
23+
contact: {
24+
email?: string | null | undefined;
25+
phone: string | undefined;
26+
};
27+
};
28+
29+
type UpdatedUser = SetNonNullableDeep<User, 'address.street' | 'contact.email' | 'contact.phone'>;
30+
//=> {
31+
// name: string;
32+
// address: {
33+
// city: string | undefined;
34+
// street?: string;
35+
// };
36+
// contact: {
37+
// email?: string;
38+
// phone: string;
39+
// };
40+
// };
41+
```
42+
43+
@example
44+
```
45+
import type {SetNonNullableDeep} from 'type-fest';
46+
47+
// Set specific indices in an array to be non-nullable.
48+
type ArrayExample1 = SetNonNullableDeep<{a: [number | null, number | null, number | undefined]}, 'a.1' | 'a.2'>;
49+
//=> {a: [number | null, number, number?]}
50+
51+
// Optional modifier (`?`) is not removed.
52+
type ArrayExample2 = SetNonNullableDeep<{a: [(number | null)?, (number | null)?]}, 'a.1'>;
53+
//=> {a: [(number | null)?, number?]}
54+
```
55+
56+
@category Object
57+
*/
58+
export type SetNonNullableDeep<BaseType, KeyPaths extends Paths<BaseType>> =
59+
SetNonNullableDeepHelper<BaseType, UnionToTuple<KeyPaths>>;
60+
61+
/**
62+
Internal helper for {@link SetNonNullableDeep}.
63+
64+
Recursively transforms the `BaseType` by applying {@link SetNonNullableDeepSinglePath} for each path in `KeyPathsTuple`.
65+
*/
66+
type SetNonNullableDeepHelper<BaseType, KeyPathsTuple extends UnknownArray> =
67+
KeyPathsTuple extends [infer KeyPath, ...infer RestPaths]
68+
? SetNonNullableDeepHelper<SetNonNullableDeepSinglePath<BaseType, KeyPath>, RestPaths>
69+
: BaseType;
70+
71+
/**
72+
Makes a single path non-nullable in `BaseType`.
73+
*/
74+
type SetNonNullableDeepSinglePath<BaseType, KeyPath> =
75+
BaseType extends NonRecursiveType | ReadonlySet<unknown> | ReadonlyMap<unknown, unknown> // Also distributes `BaseType`
76+
? BaseType
77+
: KeyPath extends `${infer Property}.${infer RestPath}`
78+
? {
79+
[Key in keyof BaseType]: Property extends `${Key & (string | number)}`
80+
? SetNonNullableDeepSinglePath<BaseType[Key], RestPath>
81+
: BaseType[Key];
82+
}
83+
: Simplify<SetNonNullable<BaseType, (KeyPath | StringToNumber<KeyPath & string>) & keyof BaseType>>;

test-d/set-non-nullable-deep.ts

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import {expectType} from 'tsd';
2+
import type {SetNonNullableDeep} from '../source/set-non-nullable-deep';
3+
4+
expectType<{a: number}>({} as SetNonNullableDeep<{a: number | null}, 'a'>);
5+
expectType<{a: number}>({} as SetNonNullableDeep<{a: number | undefined}, 'a'>);
6+
expectType<{a: number}>({} as SetNonNullableDeep<{a: number | null | undefined}, 'a'>);
7+
expectType<{a?: number}>({} as SetNonNullableDeep<{a?: number | null}, 'a'>);
8+
expectType<{a?: number}>({} as SetNonNullableDeep<{a?: number | undefined}, 'a'>);
9+
expectType<{a?: number}>({} as SetNonNullableDeep<{a?: number | null | undefined}, 'a'>);
10+
11+
expectType<{a: number; b: {c: string}}>({} as SetNonNullableDeep<{a: number; b: {c: string | null}}, 'b.c'>);
12+
expectType<{a: number; b: {c: string}}>({} as SetNonNullableDeep<{a: number; b: {c: string | undefined}}, 'b.c'>);
13+
expectType<{a: number; b: {c: string}}>({} as SetNonNullableDeep<{a: number; b: {c: string | null | undefined}}, 'b.c'>);
14+
expectType<{a: number; b: {c?: string}}>({} as SetNonNullableDeep<{a: number; b: {c?: string | null}}, 'b.c'>);
15+
expectType<{a: number; b: {c?: string}}>({} as SetNonNullableDeep<{a: number; b: {c?: string | undefined}}, 'b.c'>);
16+
expectType<{a: number; b: {c?: string}}>({} as SetNonNullableDeep<{a: number; b: {c?: string | null | undefined}}, 'b.c'>);
17+
18+
// Becomes `never` when the value is only `null` or `undefined`
19+
expectType<{a: never}>({} as SetNonNullableDeep<{a: null | undefined}, 'a'>);
20+
expectType<{a?: never}>({} as SetNonNullableDeep<{a?: null | undefined}, 'a'>);
21+
22+
// Ignores keys that are already non-nullable
23+
expectType<{a: number}>({} as SetNonNullableDeep<{a: number}, 'a'>);
24+
expectType<{readonly a?: number}>({} as SetNonNullableDeep<{readonly a?: number}, 'a'>);
25+
expectType<{a?: number; b: {c: string}}>({} as SetNonNullableDeep<{a?: number; b: {c: string}}, 'b.c'>);
26+
expectType<{a: number; readonly b?: {c?: string}}>({} as SetNonNullableDeep<{a: number; readonly b?: {c?: string}}, 'b.c'>);
27+
28+
// Removes nullables only from the specified keys, nullables in other paths are preserved
29+
expectType<{a: number; b: {c: string | null | undefined} | null | undefined}>(
30+
{} as SetNonNullableDeep<{a: number | null | undefined; b: {c: string | null | undefined} | null | undefined}, 'a'>,
31+
);
32+
expectType<{a: number | null | undefined; b: {c: string | null | undefined}}>(
33+
{} as SetNonNullableDeep<{a: number | null | undefined; b: {c: string | null | undefined} | null | undefined}, 'b'>,
34+
);
35+
expectType<{a: number | null | undefined; b: {c: string} | null | undefined}>(
36+
{} as SetNonNullableDeep<{a: number | null | undefined; b: {c: string | null | undefined} | null | undefined}, 'b.c'>,
37+
);
38+
expectType<{a: {b: string}}>({} as SetNonNullableDeep<{a: {b: string | null} | null | undefined}, 'a' | 'a.b'>);
39+
expectType<{a: {b: {c: string}} | null | undefined}>({} as SetNonNullableDeep<{a: {b: {c: string | undefined} | null} | null | undefined}, 'a.b' | 'a.b.c'>);
40+
expectType<{a: {b: {c: string} | null}}>({} as SetNonNullableDeep<{a: {b: {c: string | undefined} | null} | null | undefined}, 'a' | 'a.b.c'>);
41+
expectType<{a: {b?: {c: {d?: {e: number | string}; f: {g: boolean}} | undefined} | null; h: number | null}}>(
42+
{} as SetNonNullableDeep<
43+
{a: {b?: {c: {d?: {e: number | string} | null; f: {g: boolean | null}} | undefined} | null; h: number | null} | undefined},
44+
'a' | 'a.b.c.d' | 'a.b.c.f.g'
45+
>,
46+
);
47+
48+
// Unions
49+
expectType<{a: string} | {a?: number} | {a: boolean}>({} as SetNonNullableDeep<{a: string | null} | {a?: number | undefined} | {a: boolean | null | undefined}, 'a'>);
50+
expectType<{a: string; b: number}>({} as SetNonNullableDeep<{a: string | null; b: number | undefined}, 'a' | 'b'>);
51+
expectType<{a: string; b: {c: {d: {e: number}} | null} | undefined}>(
52+
{} as SetNonNullableDeep<{a: string | null; b: {c: {d: {e: number | undefined}} | null} | undefined}, 'a' | 'b.c.d.e'>,
53+
);
54+
expectType<{a: {b: string | null}} | {a?: number; b: {c?: string}}>(
55+
{} as SetNonNullableDeep<{a: {b: string | null} | undefined} | {a?: number | undefined; b: {c?: string}}, 'a' | 'b.c'>,
56+
);
57+
expectType<{a: string; b: {c?: {d: string} | undefined; f?: number}} | {a: {b: number}; c: never; d?: undefined}>(
58+
{} as SetNonNullableDeep<
59+
{a: string; b: {c?: {d: string | null} | undefined; f?: number} | null} | {a: {b: number | null}; c: null; d?: undefined},
60+
'b' | 'b.c.d' | 'a.b' | 'c'
61+
>,
62+
);
63+
expectType<{a: 1; b: {c: 2}; d?: {e?: {f?: 2}; g?: 3}}>(
64+
{} 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'>,
65+
);
66+
expectType<{a: {b: string} | {c: string} | {b: {c: string | null}}}>(
67+
{} as SetNonNullableDeep<{a: {b: string | null} | {c: string} | {b: {c: string | null} | undefined}}, 'a.b'>,
68+
);
69+
70+
// Preserves non-nullable values when they are in a union with objects
71+
expectType<{a?: {b: string} | number}>({} as SetNonNullableDeep<{a?: {b: string} | number | null | undefined}, 'a'>);
72+
expectType<{a: {b: Array<{c?: number | null}> | number}}>({} as SetNonNullableDeep<{a: {b: Array<{c?: number | null}> | number | null}}, 'a.b'>);
73+
expectType<{a: {b: string | number} | number}>({} as SetNonNullableDeep<{a: {b: string | number | null} | number | null | undefined}, 'a' | 'a.b'>);
74+
expectType<{a: number; b: {c: number | null} | {d: string}}>({} as SetNonNullableDeep<{a: number; b: {c: number | null} | {d: string | undefined} | null}, 'b' | 'b.d'>);
75+
76+
// Preserves `readonly` modifier
77+
expectType<{a: string; b: {readonly c: number}}>({} as SetNonNullableDeep<{a: string; b: {readonly c: number | null}}, 'b.c'>);
78+
expectType<{readonly a: string; readonly b: {c: number | null}}>({} as SetNonNullableDeep<{readonly a: string | null; readonly b: {c: number | null} | null}, 'a' | 'b'>);
79+
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'>);
80+
81+
// Number keys
82+
expectType<{0: 1; 1: {2?: string}}>({} as SetNonNullableDeep<{0: 1; 1: {2?: string | null}}, '1.2'>);
83+
expectType<{0: 1; 1?: {2: string | null}}>({} as SetNonNullableDeep<{0: 1 | null; 1?: {2: string | null} | undefined}, 0 | 1>);
84+
85+
// Number keys containing dots
86+
// NOTE: Passing "1.2" instead of 1.2 will treat it as a path instead of a key
87+
expectType<{1.2?: string; 1?: {2?: string | null} | null}>({} as SetNonNullableDeep<{1.2?: string | null; 1?: {2?: string | null} | null}, 1.2>);
88+
expectType<{1.2?: string | null; 1?: {2?: string} | null}>({} as SetNonNullableDeep<{1.2?: string | null; 1?: {2?: string | null} | null}, '1.2'>);
89+
expectType<{1.2?: string; 1?: {2?: string} | undefined}>({} as SetNonNullableDeep<{1.2?: string | undefined; 1?: {2?: string | undefined} | undefined}, 1.2 | '1.2'>);
90+
91+
// Index signatures
92+
expectType<{[x: string]: any; a: number; b: {c: number}}>({} as SetNonNullableDeep<{[x: string]: any; a: number | null; b: {c: number | null}}, 'a' | 'b.c'>);
93+
94+
// Works with `KeyPaths` containing template literals
95+
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'}`>);
96+
expectType<{a: number; b: null | {readonly c: {1: number[]} | undefined} | {d: {1: number[]} | null}}>({} as SetNonNullableDeep<
97+
{a: number | undefined; b: null | {readonly c: {1: number[] | undefined} | undefined} | {d: {1: number[] | undefined} | null}}, 'a' | `b.${'c' | 'd'}.1`
98+
>);
99+
100+
// Non recursive types
101+
expectType<{a: {b: never} | Set<number | null>}>({} as SetNonNullableDeep<{a: {b: null} | Set<number | null>}, 'a.b'>);
102+
expectType<{a: {b: {c: string} | Map<string, string>}}>({} as SetNonNullableDeep<{a: {b: {c: string} | Map<string, string> | null}}, 'a.b'>);
103+
104+
// === Arrays ===
105+
expectType<[string, number | null, boolean]>({} as SetNonNullableDeep<[string | null, number | null, boolean | undefined], '0' | '2'>);
106+
expectType<{a?: [string | null, number?, boolean?] | null}>({} as SetNonNullableDeep<{a?: [string | null, (number | undefined)?, (boolean | null)?] | null}, 'a.1' | 'a.2'>);
107+
expectType<{a: readonly [string, number, (boolean | null)?]}>({} as SetNonNullableDeep<{a: readonly [string, number | undefined, (boolean | null)?]}, 'a.1'>);
108+
expectType<{readonly a: [string, number | null | undefined, boolean, ...Array<number | null>]}>(
109+
{} as SetNonNullableDeep<{readonly a: [string | null, number | null | undefined, boolean | undefined, ...Array<number | null>]}, 'a.0' | 'a.2'>,
110+
);
111+
expectType<{readonly a?: [string, number, boolean, ...Array<string | null>]}>(
112+
{} as SetNonNullableDeep<{readonly a?: [string, number | null, boolean | undefined, ...Array<string | null>] | undefined}, 'a' | 'a.1' | 'a.2'>,
113+
);
114+
115+
// Readonly arrays
116+
expectType<{a?: {b?: readonly [(string | number)?]} | null}>({} as SetNonNullableDeep<{a?: {b?: readonly [(string | number | null)?]} | null}, 'a.b.0'>);
117+
expectType<{a: readonly [string, number, boolean, ...Array<string | undefined>] | undefined}>(
118+
{} as SetNonNullableDeep<{a: readonly [string | null, number | undefined, boolean | null, ...Array<string | undefined>] | undefined}, 'a.0' | 'a.1' | 'a.2'>,
119+
);
120+
121+
// Ignores `Keys` that are already non-nullable
122+
expectType<{a: [string, (number | null)?, boolean?]}>({} as SetNonNullableDeep<{a: [string, (number | null)?, boolean?]}, 'a.0'>);
123+
expectType<{a: [string, number?, boolean?]}>({} as SetNonNullableDeep<{a: [string, (number | null)?, boolean?]}, 'a.0' | 'a.1'>);
124+
125+
// Ignores `Keys` that are not known
126+
// This case is only possible when the array contains a rest element,
127+
// because otherwise the constaint on `KeyPaths` would disallow out of bound keys.
128+
expectType<{a?: readonly [string | null, (number | undefined)?, (boolean | null)?, ...Array<number | null | undefined>] | undefined}>(
129+
{} as SetNonNullableDeep<{a?: readonly [string | null, (number | undefined)?, (boolean | null)?, ...Array<number | null | undefined>] | undefined}, 'a.10'>,
130+
);
131+
132+
// Unions of arrays
133+
expectType<{a: [string] | [string, number?, (boolean | null)?, ...Array<number | null>] | readonly [string, number, (boolean | undefined)?]}>(
134+
{} as SetNonNullableDeep<{a: [string | undefined] | [string | null, number?, (boolean | null)?, ...Array<number | null>] | readonly [string | null | undefined, number | null, (boolean | undefined)?]}, 'a.0' | 'a.1'>,
135+
);
136+
137+
// Labelled tuples
138+
expectType<{a?: [b: string, c: number] | undefined}>({} as SetNonNullableDeep<{a?: [b: string | null, c: number | undefined] | undefined}, 'a.0' | 'a.1'>);
139+
140+
// Non-tuple arrays
141+
expectType<{a: string[]}>({} as SetNonNullableDeep<{a: Array<string | null>}, `a.${number}`>);
142+
expectType<{readonly a: ReadonlyArray<string | number>}>({} as SetNonNullableDeep<{readonly a: ReadonlyArray<string | number | null> | undefined}, 'a' | `a.${number}`>);
143+
144+
// Nested arrays
145+
expectType<{a?: [([string?, (number | null)?] | null)?]}>({} as SetNonNullableDeep<{a?: [([(string | undefined)?, (number | null)?] | null)?] | undefined}, 'a' | 'a.0.0'>);
146+
expectType<{a?: [[(string | undefined)?, number?]?] | undefined}>({} as SetNonNullableDeep<{a?: [([(string | undefined)?, (number | null)?] | null)?] | undefined}, 'a.0.1' | 'a.0'>);
147+
expectType<{a?: [[string | null, number]?] | null}>({} as SetNonNullableDeep<{a?: [([string | null, number | undefined] | null)?] | null}, 'a.0' | 'a.0.1'>);
148+
expectType<{a?: Array<[string | null, number?]> | null}>({} as SetNonNullableDeep<{a?: Array<[string | null, (number | undefined)?]> | null}, `a.${number}.1`>);
149+
150+
// Removes `null` & `undefined` from keys inside arrays
151+
expectType<{a?: Array<{b: number}> | undefined}>({} as SetNonNullableDeep<{a?: Array<{b: number | null}> | undefined}, `a.${number}.b`>);
152+
expectType<{readonly a?: [{readonly b: number}]}>({} as SetNonNullableDeep<{readonly a?: [{readonly b: number | undefined}] | null}, 'a' | 'a.0' | 'a.0.b'>);
153+
expectType<{readonly a: [{readonly b: number}, {c?: string | null}?]}>(
154+
{} as SetNonNullableDeep<{readonly a: [{readonly b: number | null | undefined}, ({c?: string | null} | undefined)?]}, 'a.0.b' | 'a.1' >,
155+
);
156+
expectType<{a?: Array<{b: number; c?: string | null}> | null}>({} as SetNonNullableDeep<{a?: Array<{b: number | undefined; c?: string | null}> | null}, `a.${number}.b`>);
157+
expectType<{a: [{b?: number | null; readonly c: string}]}>({} as SetNonNullableDeep<{a: [{b?: number | null; readonly c: string | undefined}]}, 'a.0.c'>);

0 commit comments

Comments
 (0)