Skip to content

Commit bf5ce3c

Browse files
authored
SetRequiredDeep: Fix handling of unions in nested keys (#1037)
1 parent c6d5142 commit bf5ce3c

File tree

2 files changed

+177
-33
lines changed

2 files changed

+177
-33
lines changed

source/set-required-deep.d.ts

+33-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import type {IsAny} from './is-any';
12
import type {NonRecursiveType, StringToNumber} from './internal';
23
import type {Paths} from './paths';
4+
import type {SetRequired} from './set-required';
35
import type {SimplifyDeep} from './simplify-deep';
6+
import type {UnionToTuple} from './union-to-tuple';
7+
import type {RequiredDeep} from './required-deep';
48
import type {UnknownArray} from './unknown-array';
59

610
/**
@@ -28,19 +32,37 @@ type SomeRequiredDeep = SetRequiredDeep<Foo, 'a' | `c.${number}.d`>;
2832
// d: number // Is now required
2933
// }[]
3034
// }
35+
36+
// Set specific indices in an array to be required.
37+
type ArrayExample = SetRequiredDeep<{a: [number?, number?, number?]}, 'a.0' | 'a.1'>;
38+
//=> {a: [number, number, number?]}
3139
```
3240
3341
@category Object
3442
*/
35-
export type SetRequiredDeep<BaseType, KeyPaths extends Paths<BaseType>> =
36-
BaseType extends NonRecursiveType
43+
export type SetRequiredDeep<BaseType, KeyPaths extends Paths<BaseType>> = IsAny<KeyPaths> extends true
44+
? SimplifyDeep<RequiredDeep<BaseType>>
45+
: SetRequiredDeepHelper<BaseType, UnionToTuple<KeyPaths>>;
46+
47+
/**
48+
Internal helper for {@link SetRequiredDeep}.
49+
50+
Recursively transforms the `BaseType` by applying {@link SetRequiredDeepSinglePath} for each path in `KeyPathsTuple`.
51+
*/
52+
type SetRequiredDeepHelper<BaseType, KeyPathsTuple extends UnknownArray> =
53+
KeyPathsTuple extends [infer KeyPath, ...infer RestPaths]
54+
? SetRequiredDeepHelper<SetRequiredDeepSinglePath<BaseType, KeyPath>, RestPaths>
55+
: BaseType;
56+
57+
/**
58+
Makes a single path required in `BaseType`.
59+
*/
60+
type SetRequiredDeepSinglePath<BaseType, KeyPath> = BaseType extends NonRecursiveType
3761
? BaseType
38-
: SimplifyDeep<(
39-
BaseType extends UnknownArray
40-
? {}
41-
: {[K in keyof BaseType as K extends (KeyPaths | StringToNumber<KeyPaths & string>) ? K : never]-?: BaseType[K]}
42-
) & {
43-
[K in keyof BaseType]: Extract<KeyPaths, `${K & (string | number)}.${string}`> extends never
44-
? BaseType[K]
45-
: SetRequiredDeep<BaseType[K], KeyPaths extends `${K & (string | number)}.${infer Rest extends Paths<BaseType[K]>}` ? Rest : never>
46-
}>;
62+
: KeyPath extends `${infer Property}.${infer RestPath}`
63+
? {
64+
[Key in keyof BaseType]: Property extends `${Key & (string | number)}`
65+
? SetRequiredDeepSinglePath<BaseType[Key], RestPath>
66+
: BaseType[Key];
67+
}
68+
: SetRequired<BaseType, (KeyPath | StringToNumber<KeyPath & string>) & keyof BaseType>;

test-d/set-required-deep.ts

+144-22
Original file line numberDiff line numberDiff line change
@@ -21,42 +21,164 @@ expectType<{a: number; b?: {c?: string}}>(variation4);
2121
declare const variation5: SetRequiredDeep<{a?: '1'; b?: {c?: boolean}} | {a?: '2'; b?: {c?: boolean}}, 'a'>;
2222
expectType<{a: '1'; b?: {c?: boolean}} | {a: '2'; b?: {c?: boolean}}>(variation5);
2323

24-
// Set array key to required
24+
// Set key with array type to required
2525
declare const variation6: SetRequiredDeep<{a?: Array<{b?: number}>}, 'a'>;
2626
expectType<{a: Array<{b?: number}>}>(variation6);
2727

28-
// Set key inside array to required
29-
declare const variation7: SetRequiredDeep<{a?: Array<{b?: number}>}, `a.${number}.b`>;
30-
expectType<{a?: Array<{b: number}>}>(variation7);
31-
32-
// Set only specified keys inside array to required
33-
declare const variation8: SetRequiredDeep<{a?: Array<{b?: number; c?: string}>}, `a.${number}.b`>;
34-
expectType<{a?: Array<{b: number; c?: string}>}>(variation8);
35-
3628
// Can set both root and nested keys to required
37-
declare const variation9: SetRequiredDeep<{a?: number; b?: {c?: string}}, 'b' | 'b.c'>;
38-
expectType<{a?: number; b: {c: string}}>(variation9);
29+
declare const variation7: SetRequiredDeep<{a?: number; b?: {c?: string}}, 'b' | 'b.c'>;
30+
expectType<{a?: number; b: {c: string}}>(variation7);
3931

4032
// Preserves required root keys
41-
declare const variation10: SetRequiredDeep<{a: 1; b: {c?: 1}}, 'b.c'>;
42-
expectType<{a: 1; b: {c: 1}}>(variation10);
33+
declare const variation8: SetRequiredDeep<{a: 1; b: {c?: 1}}, 'b.c'>;
34+
expectType<{a: 1; b: {c: 1}}>(variation8);
4335

4436
// Preserves union in root keys
45-
declare const variation11: SetRequiredDeep<{a: 1; b: {c?: 1} | number}, 'b.c'>;
46-
expectType<{a: 1; b: {c: 1} | number}>(variation11);
37+
declare const variation9: SetRequiredDeep<{a: 1; b: {c?: 1} | number}, 'b.c'>;
38+
expectType<{a: 1; b: {c: 1} | number}>(variation9);
39+
40+
// Preserves readonly
41+
declare const variation10: SetRequiredDeep<{a: 1; readonly b: {c?: 1}}, 'b.c'>;
42+
expectType<{a: 1; readonly b: {c: 1}}>(variation10);
4743

48-
// Preserves readonly in root keys
49-
declare const variation12: SetRequiredDeep<{a: 1; readonly b: {c?: 1}}, 'b.c'>;
50-
expectType<{a: 1; readonly b: {c: 1}}>(variation12);
44+
declare const variation11: SetRequiredDeep<{readonly a?: 1; readonly b?: {readonly c?: 1}}, 'a' | 'b'>;
45+
expectType<{readonly a: 1; readonly b: {readonly c?: 1}}>(variation11);
46+
47+
declare const variation12: SetRequiredDeep<{readonly a?: 1; readonly b?: {readonly c?: 1}}, 'a' | 'b' | 'b.c'>;
48+
expectType<{readonly a: 1; readonly b: {readonly c: 1}}>(variation12);
5149

5250
// Works with number keys
5351
declare const variation13: SetRequiredDeep<{0: 1; 1: {2?: string}}, '1.2'>;
5452
expectType<{0: 1; 1: {2: string}}>(variation13);
5553

54+
declare const variation14: SetRequiredDeep<{0?: 1; 1?: {2?: string}}, 0 | 1>;
55+
expectType<{0: 1; 1: {2?: string}}>(variation14);
56+
5657
// Multiple keys
57-
declare const variation14: SetRequiredDeep<{a?: 1; b?: {c?: 2}; d?: {e?: {f?: 2}; g?: 3}}, 'a' | 'b' | 'b.c' | 'd.e.f' | 'd.g'>;
58-
expectType<{a: 1; b: {c: 2}; d?: {e?: {f: 2}; g: 3}}>(variation14);
58+
declare const variation15: SetRequiredDeep<{a?: 1; b?: {c?: 2}; d?: {e?: {f?: 2}; g?: 3}}, 'a' | 'b' | 'b.c' | 'd.e.f' | 'd.g'>;
59+
expectType<{a: 1; b: {c: 2}; d?: {e?: {f: 2}; g: 3}}>(variation15);
5960

6061
// Index signatures
61-
declare const variation15: SetRequiredDeep<{[x: string]: any; a?: number; b?: {c?: number}}, 'a' | 'b.c'>;
62-
expectType<{[x: string]: any; a: number; b?: {c: number}}>(variation15);
62+
declare const variation16: SetRequiredDeep<{[x: string]: any; a?: number; b?: {c?: number}}, 'a' | 'b.c'>;
63+
expectType<{[x: string]: any; a: number; b?: {c: number}}>(variation16);
64+
65+
// Preserves union in nested keys
66+
declare const variation17: SetRequiredDeep<{a: 1; b?: {c?: 1} | number}, 'b'>;
67+
expectType<{a: 1; b: {c?: 1} | number}>(variation17);
68+
69+
declare const variation18: SetRequiredDeep<{a?: number; b?: {c?: number} | {d?: string}}, 'b' | 'b.d'>;
70+
expectType<{a?: number; b: {c?: number} | {d: string}}>(variation18);
71+
72+
// Works with number keys containing dots
73+
// NOTE: Passing "1.2" instead of 1.2 will treat it as a path instead of a key
74+
declare const variation19: SetRequiredDeep<{1.2?: string; 1?: {2?: string}}, 1.2>;
75+
expectType<{1.2: string; 1?: {2?: string}}>(variation19);
76+
77+
declare const variation20: SetRequiredDeep<{1.2?: string; 1?: {2?: string}}, '1.2'>;
78+
expectType<{1.2?: string; 1?: {2: string}}>(variation20);
79+
80+
declare const variation21: SetRequiredDeep<{1.2?: string; 1?: {2?: string}}, 1.2 | '1.2'>;
81+
expectType<{1.2: string; 1?: {2: string}}>(variation21);
82+
83+
// Works with unions
84+
declare const variation22: SetRequiredDeep<{a?: {readonly b?: number}} | {readonly b?: {c?: number[]}}, 'a.b' | 'b' | 'b.c'>;
85+
expectType<{a?: {readonly b: number}} | {readonly b: {c: number[]}}>(variation22);
86+
87+
// Works with `KeyPaths` containing template literals
88+
declare const variation23: SetRequiredDeep<{a?: number; b?: {c?: number} | {d?: number}}, `b.${'c' | 'd'}`>;
89+
expectType<{a?: number; b?: {c: number} | {d: number}}>(variation23);
90+
91+
declare const variation24: SetRequiredDeep<
92+
{a?: number; b?: {readonly c?: {1?: number}} | {d?: {1?: number}}}, 'a' | `b.${'c' | 'd'}.1`
93+
>;
94+
expectType<{a: number; b?: {readonly c?: {1: number}} | {d?: {1: number}}}>(variation24);
95+
96+
// Calls `RequiredDeep` when `KeyPaths` is `any`
97+
declare const variation25: SetRequiredDeep<{a?: number; readonly b?: {c?: string}}, any>;
98+
expectType<{a: number; readonly b: {c: string}}>(variation25);
99+
100+
// Does nothing when `KeyPaths` is `never`
101+
declare const variation26: SetRequiredDeep<{a?: number; readonly b?: {c?: string}}, never>;
102+
expectType<{a?: number; readonly b?: {c?: string}}>(variation26);
103+
104+
// =================
105+
// Works with arrays
106+
// =================
107+
108+
// All optional elements
109+
expectType<{a?: [string, number, boolean?]}>({} as SetRequiredDeep<{a?: [string?, number?, boolean?]}, 'a.0' | 'a.1'>);
110+
111+
// Mix of optional and required elements
112+
expectType<{a: readonly [string, number, boolean]}>({} as SetRequiredDeep<{a: readonly [string, number?, boolean?]}, 'a.1' | 'a.2'>);
113+
114+
// Mix of optional and rest elements
115+
expectType<{readonly a: [string, number, boolean?, ...number[]]}>({} as SetRequiredDeep<{readonly a: [string?, number?, boolean?, ...number[]]}, 'a.0' | 'a.1'>);
116+
117+
// Mix of optional, required, and rest elements
118+
expectType<{readonly a?: [string, number, boolean, ...string[]]}>({} as SetRequiredDeep<{readonly a?: [string, number?, boolean?, ...string[]]}, 'a.1' | 'a.2'>);
119+
120+
// Works with readonly arrays
121+
expectType<{a?: {b?: readonly [(string | number)]}}>({} as SetRequiredDeep<{a?: {b?: readonly [(string | number)?]}}, 'a.b.0'>);
122+
expectType<{a: readonly [string, number, boolean, ...string[]]}>(
123+
{} as SetRequiredDeep<{a?: readonly [string, number?, boolean?, ...string[]]}, 'a' | 'a.1' | 'a.2'>,
124+
);
125+
126+
// Ignores `Keys` that are already required
127+
expectType<{a: [string, number?, boolean?]}>({} as SetRequiredDeep<{a: [string, number?, boolean?]}, 'a.0'>);
128+
129+
// Ignores `Keys` that are not known
130+
// This case is only possible when the array contains a rest element,
131+
// because otherwise the constaint on `KeyPaths` would disallow out of bound keys.
132+
expectType<{a?: readonly [string?, number?, boolean?, ...number[]]}>(
133+
{} as SetRequiredDeep<{a?: readonly [string?, number?, boolean?, ...number[]]}, 'a.10'>,
134+
);
135+
136+
// Marks all keys as required, if `Keys` is `number`.
137+
// This case is only possible when the array contains a rest element,
138+
// because otherwise the constaint on `KeyPaths` would be stricter.
139+
expectType<{a?: readonly [string, number, boolean, ...number[]]}>(
140+
{} as SetRequiredDeep<{a?: readonly [string?, number?, boolean?, ...number[]]}, `a.${number}`>,
141+
);
142+
143+
// Preserves `| undefined`, similar to how built-in `Required` works.
144+
expectType<{a: [string | undefined, number | undefined, boolean]}>({} as SetRequiredDeep<{a: [string | undefined, (number | undefined)?, boolean?]}, 'a.0' | 'a.1' | 'a.2'>);
145+
expectType<{a: readonly [string | undefined, (number | undefined)?, boolean?]}>(
146+
{} as SetRequiredDeep<{a: readonly [(string | undefined)?, (number | undefined)?, boolean?]}, 'a.0'>,
147+
);
148+
149+
// Optional elements cannot appear after required ones, `Keys` leading to such situations are ignored.
150+
expectType<{a: [string?, number?, boolean?]}>({} as SetRequiredDeep<{a: [string?, number?, boolean?]}, 'a.1' | 'a.2'>); // `a.1` and `a.2` can't be required when `a.0` is optional
151+
expectType<{a: [string, number, boolean?, string?, string?]}>(
152+
{} as SetRequiredDeep<{a: [string?, number?, boolean?, string?, string?]}, 'a.0' | 'a.1' | 'a.3'>, // `a.3` can't be required when `a.2` is optional
153+
);
154+
expectType<{a: readonly [string | undefined, number?, boolean?, ...string[]]}>(
155+
{} as SetRequiredDeep<{a?: readonly [string | undefined, number?, boolean?, ...string[]]}, 'a' | 'a.2'>, // `a.2` can't be required when `a.1` is optional
156+
);
157+
158+
// Works with unions of arrays
159+
expectType<{a: [string] | [string, number, boolean?, ...number[]] | readonly [string, number, boolean?]}>(
160+
{} as SetRequiredDeep<{a: [string?] | [string, number?, boolean?, ...number[]] | readonly [string, number?, boolean?]}, 'a.0' | 'a.1'>,
161+
);
162+
163+
// Works with labelled tuples
164+
expectType<{a?: [b: string, c: number]}>({} as SetRequiredDeep<{a?: [b?: string, c?: number]}, 'a.0' | 'a.1'>);
165+
166+
// Non tuple arrays are left unchanged
167+
expectType<{a: string[]}>({} as SetRequiredDeep<{a: string[]}, `a.${number}`>);
168+
expectType<{readonly a: ReadonlyArray<string | number>}>({} as SetRequiredDeep<{readonly a?: ReadonlyArray<string | number>}, 'a' | `a.${number}`>);
169+
170+
// Works with nested arrays
171+
expectType<{a?: [[string, number?]?]}>({} as SetRequiredDeep<{a?: [[string?, number?]?]}, 'a.0.0'>);
172+
expectType<{a?: [[string, number]]}>({} as SetRequiredDeep<{a?: [[string?, number?]?]}, 'a.0' | 'a.0.0' | 'a.0.1'>);
173+
expectType<{a?: Array<[string, number?]>}>({} as SetRequiredDeep<{a?: Array<[string?, number?]>}, `a.${number}.0`>);
174+
175+
// Set key inside array to required
176+
expectType<{a?: Array<{b: number}>}>({} as SetRequiredDeep<{a?: Array<{b?: number}>}, `a.${number}.b`>);
177+
expectType<{readonly a?: [{readonly b: number}]}>({} as SetRequiredDeep<{readonly a?: [{readonly b?: number}]}, 'a.0.b'>);
178+
expectType<{readonly a: [{readonly b: number}, {c?: string}]}>(
179+
{} as SetRequiredDeep<{readonly a?: [{readonly b?: number}, {c?: string}?]}, 'a' | 'a.0.b' | 'a.1' >,
180+
);
181+
182+
// Set only specified keys inside array to required
183+
expectType<{a?: Array<{b: number; c?: string}>}>({} as SetRequiredDeep<{a?: Array<{b?: number; c?: string}>}, `a.${number}.b`>);
184+
expectType<{a: [{b?: number; readonly c: string}]}>({} as SetRequiredDeep<{a: [{b?: number; readonly c?: string}]}, 'a.0.c'>);

0 commit comments

Comments
 (0)