Skip to content

Commit c897aad

Browse files
authored
SetRequired: Fix support for removal of optional modifiers from tuples (#1030)
1 parent 278df80 commit c897aad

File tree

5 files changed

+238
-9
lines changed

5 files changed

+238
-9
lines changed

source/internal/array.d.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type {IfNever} from '../if-never';
12
import type {UnknownArray} from '../unknown-array';
23

34
/**
@@ -90,4 +91,36 @@ T extends readonly [...infer U] ?
9091
/**
9192
Returns whether the given array `T` is readonly.
9293
*/
93-
export type IsArrayReadonly<T extends UnknownArray> = T extends unknown[] ? false : true;
94+
export type IsArrayReadonly<T extends UnknownArray> = IfNever<T, false, T extends unknown[] ? false : true>;
95+
96+
/**
97+
An if-else-like type that resolves depending on whether the given array is readonly.
98+
99+
@see {@link IsArrayReadonly}
100+
101+
@example
102+
```
103+
import type {ArrayTail} from 'type-fest';
104+
105+
type ReadonlyPreservingArrayTail<TArray extends readonly unknown[]> =
106+
ArrayTail<TArray> extends infer Tail
107+
? IfArrayReadonly<TArray, Readonly<Tail>, Tail>
108+
: never;
109+
110+
type ReadonlyTail = ReadonlyPreservingArrayTail<readonly [string, number, boolean]>;
111+
//=> readonly [number, boolean]
112+
113+
type NonReadonlyTail = ReadonlyPreservingArrayTail<[string, number, boolean]>;
114+
//=> [number, boolean]
115+
116+
type ShouldBeTrue = IfArrayReadonly<readonly unknown[]>;
117+
//=> true
118+
119+
type ShouldBeBar = IfArrayReadonly<unknown[], 'foo', 'bar'>;
120+
//=> 'bar'
121+
```
122+
*/
123+
export type IfArrayReadonly<T extends UnknownArray, TypeIfArrayReadonly = true, TypeIfNotArrayReadonly = false> =
124+
IsArrayReadonly<T> extends infer Result
125+
? Result extends true ? TypeIfArrayReadonly : TypeIfNotArrayReadonly
126+
: never; // Should never happen

source/set-required.d.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type {Except} from './except';
2-
import type {HomomorphicPick} from './internal';
2+
import type {HomomorphicPick, IfArrayReadonly} from './internal';
33
import type {KeysOfUnion} from './keys-of-union';
4+
import type {OptionalKeysOf} from './optional-keys-of';
45
import type {Simplify} from './simplify';
6+
import type {UnknownArray} from './unknown-array';
57

68
/**
79
Create a type that makes the given keys required. The remaining keys are kept as is. The sister of the `SetOptional` type.
@@ -24,19 +26,46 @@ type SomeRequired = SetRequired<Foo, 'b' | 'c'>;
2426
// b: string; // Was already required and still is.
2527
// c: boolean; // Is now required.
2628
// }
29+
30+
// Set specific indices in an array to be required.
31+
type ArrayExample = SetRequired<[number?, number?, number?], 0 | 1>;
32+
//=> [number, number, number?]
2733
```
2834
2935
@category Object
3036
*/
3137
export type SetRequired<BaseType, Keys extends keyof BaseType> =
32-
// `extends unknown` is always going to be the case and is used to convert any
33-
// union into a [distributive conditional
34-
// type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types).
35-
BaseType extends unknown
36-
? Simplify<
38+
BaseType extends UnknownArray
39+
? SetArrayRequired<BaseType, Keys> extends infer ResultantArray
40+
? IfArrayReadonly<BaseType, Readonly<ResultantArray>, ResultantArray>
41+
: never
42+
: Simplify<
3743
// Pick just the keys that are optional from the base type.
3844
Except<BaseType, Keys> &
3945
// Pick the keys that should be required from the base type and make them required.
4046
Required<HomomorphicPick<BaseType, Keys & KeysOfUnion<BaseType>>>
41-
>
42-
: never;
47+
>;
48+
49+
/**
50+
Remove the optional modifier from the specified keys in an array.
51+
*/
52+
type SetArrayRequired<
53+
TArray extends UnknownArray,
54+
Keys,
55+
Counter extends any[] = [],
56+
Accumulator extends UnknownArray = [],
57+
> = TArray extends unknown // For distributing `TArray` when it's a union
58+
? keyof TArray & `${number}` extends never
59+
// Exit if `TArray` is empty (e.g., []), or
60+
// `TArray` contains no non-rest elements preceding the rest element (e.g., `[...string[]]` or `[...string[], string]`).
61+
? [...Accumulator, ...TArray]
62+
: TArray extends readonly [(infer First)?, ...infer Rest]
63+
? '0' extends OptionalKeysOf<TArray> // If the first element of `TArray` is optional
64+
? `${Counter['length']}` extends `${Keys & (string | number)}` // If the current index needs to be required
65+
? SetArrayRequired<Rest, Keys, [...Counter, any], [...Accumulator, First]>
66+
// If the current element is optional, but it doesn't need to be required,
67+
// then we can exit early, since no further elements can now be made required.
68+
: [...Accumulator, ...TArray]
69+
: SetArrayRequired<Rest, Keys, [...Counter, any], [...Accumulator, TArray[0]]>
70+
: never // Should never happen, since `[(infer F)?, ...infer R]` is a top-type for arrays.
71+
: never; // Should never happen
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {expectType} from 'tsd';
2+
import type {IfArrayReadonly} from '../../source/internal';
3+
4+
// Non-readonly arrays
5+
expectType<IfArrayReadonly<[]>>(false);
6+
expectType<IfArrayReadonly<number[], string, number>>({} as number);
7+
expectType<IfArrayReadonly<[string?, number?], string>>(false);
8+
expectType<IfArrayReadonly<[string, number, ...string[]], false, true>>(true);
9+
10+
// Readonly arrays
11+
expectType<IfArrayReadonly<readonly []>>(true);
12+
expectType<IfArrayReadonly<readonly number[], string, number>>({} as string);
13+
expectType<IfArrayReadonly<readonly [string?, number?], string>>({} as string);
14+
expectType<IfArrayReadonly<readonly [string, number, ...string[]], false, true>>(false);
15+
16+
// Union
17+
expectType<IfArrayReadonly<[] | [string, number]>>(false);
18+
expectType<IfArrayReadonly<[] | [string, number], string, number>>({} as number);
19+
expectType<IfArrayReadonly<readonly [] | readonly [string, number]>>(true);
20+
expectType<IfArrayReadonly<readonly [] | readonly [string, number], string, number>>({} as string);
21+
22+
// Returns union of `TypeIfArrayReadonly` and `TypeIfNotArrayReadonly` when `T` is a union of readonly and non-readonly arrays.
23+
expectType<IfArrayReadonly<[] | readonly []>>({} as boolean);
24+
expectType<IfArrayReadonly<[string, number] | readonly [string, number, ...string[]], string, number>>({} as string | number);
25+
expectType<IfArrayReadonly<[string, number] | readonly [string, number, ...string[]], string>>({} as string | false);
26+
27+
// Returns union of `TypeIfArrayReadonly` and `TypeIfNotArrayReadonly` when `T` is `any`.
28+
expectType<IfArrayReadonly<any>>({} as boolean);
29+
expectType<IfArrayReadonly<any, string, number>>({} as string | number);
30+
expectType<IfArrayReadonly<any, string>>({} as string | false);
31+
32+
// Returns `TypeIfNotArrayReadonly` when `T` is `never`.
33+
expectType<IfArrayReadonly<never>>(false);
34+
expectType<IfArrayReadonly<never, string, number>>({} as number);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {expectType} from 'tsd';
2+
import type {IsArrayReadonly} from '../../source/internal';
3+
4+
// Non-readonly arrays
5+
expectType<IsArrayReadonly<[]>>(false);
6+
expectType<IsArrayReadonly<number[]>>(false);
7+
expectType<IsArrayReadonly<[string, number?, ...string[]]>>(false);
8+
expectType<IsArrayReadonly<[x: number, y: number, z?: number]>>(false);
9+
expectType<IsArrayReadonly<[...string[], number, string]>>(false);
10+
11+
// Readonly arrays
12+
expectType<IsArrayReadonly<readonly []>>(true);
13+
expectType<IsArrayReadonly<readonly number[]>>(true);
14+
expectType<IsArrayReadonly<readonly [string, number?, ...string[]]>>(true);
15+
expectType<IsArrayReadonly<readonly [x: number, y: number, z?: number]>>(true);
16+
expectType<IsArrayReadonly<readonly [...string[], number, string]>>(true);
17+
18+
// Union
19+
expectType<IsArrayReadonly<[] | readonly []>>({} as boolean);
20+
expectType<IsArrayReadonly<[string, number] | readonly [string, number, ...string[]]>>({} as boolean);
21+
expectType<IsArrayReadonly<[] | [string, number]>>(false);
22+
expectType<IsArrayReadonly<readonly [] | readonly [string, number]>>(true);
23+
24+
// Boundary types
25+
expectType<IsArrayReadonly<any>>({} as boolean);
26+
expectType<IsArrayReadonly<never>>(false);

test-d/set-required.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,110 @@ expectType<{a?: number; readonly b?: string; readonly c: boolean}>(variation9);
4040
// Works with index signatures
4141
declare const variation10: SetRequired<{[k: string]: unknown; a?: number; b: string}, 'a' | 'b'>;
4242
expectType<{[k: string]: unknown; a: number; b: string}>(variation10);
43+
44+
// =================
45+
// Works with arrays
46+
// =================
47+
48+
// Empty array
49+
expectType<[]>({} as SetRequired<[], never>);
50+
expectType<readonly []>({} as SetRequired<readonly [], never>);
51+
52+
// All optional elements
53+
expectType<[string, number?]>({} as SetRequired<[string?, number?], '0'>);
54+
expectType<[string, number, boolean]>({} as SetRequired<[string?, number?, boolean?], '0' | '1' | '2'>);
55+
expectType<[(string | number)]>({} as SetRequired<[(string | number)?], '0'>);
56+
57+
// Works with number `Keys`, string `Keys`, and union of them.
58+
expectType<[string, number, boolean?]>({} as SetRequired<[string, number?, boolean?], 1>);
59+
expectType<[string, number, boolean, ...number[]]>({} as SetRequired<[string, number?, boolean?, ...number[]], '1' | '2'>);
60+
expectType<readonly [string, number, boolean]>({} as SetRequired<readonly [string?, number?, boolean?], '0' | 1 | 2>);
61+
62+
// Mix of optional and required elements
63+
expectType<[string, number, boolean?]>({} as SetRequired<[string, number?, boolean?], '1'>);
64+
expectType<readonly [string, number, boolean]>({} as SetRequired<readonly [string, number?, boolean?], '1' | '2'>);
65+
66+
// Mix of optional and rest elements
67+
expectType<[string, number?, boolean?, ...number[]]>({} as SetRequired<[string?, number?, boolean?, ...number[]], '0'>);
68+
expectType<[string, number, boolean, ...number[]]>({} as SetRequired<[string?, number?, boolean?, ...number[]], '0' | '1' | 2>);
69+
70+
// Mix of optional, required, and rest elements
71+
expectType<readonly [string, number, boolean?, ...number[]]>({} as SetRequired<readonly [string, number?, boolean?, ...number[]], '1'>);
72+
expectType<[string, number, boolean, ...string[]]>({} as SetRequired<[string, number?, boolean?, ...string[]], '1' | 2>);
73+
74+
// Works with readonly arrays
75+
expectType<readonly [(string | number)]>({} as SetRequired<readonly [(string | number)?], '0'>);
76+
expectType<readonly [string, number, boolean?]>({} as SetRequired<readonly [string, number?, boolean?], '1'>);
77+
expectType<readonly [string, number, boolean, ...number[]]>({} as SetRequired<readonly [string?, number?, boolean?, ...number[]], '0' | '1' | 2>);
78+
expectType<readonly [string, number, boolean, ...string[]]>({} as SetRequired<readonly [string, number?, boolean?, ...string[]], '1' | 2>);
79+
80+
// Ignores `Keys` that are already required
81+
expectType<[string, number?, boolean?]>({} as SetRequired<[string, number?, boolean?], '0'>);
82+
expectType<readonly [string, number, boolean]>({} as SetRequired<readonly [string, number, boolean], 1 | 2>);
83+
expectType<readonly [string, number, boolean, ...number[]]>({} as SetRequired<readonly [string, number, boolean, ...number[]], 1 | 2>);
84+
expectType<[string, number, boolean, ...number[]]>({} as SetRequired<[string, number?, boolean?, ...number[]], '0' | '1' | '2'>);
85+
86+
// Ignores `Keys` that are out of bounds
87+
expectType<[]>({} as SetRequired<[], 1>);
88+
expectType<[string, number?, boolean?]>({} as SetRequired<[string, number?, boolean?], 10>);
89+
expectType<[string, number, boolean]>({} as SetRequired<[string?, number?, boolean?], 0 | 1 | 2 | 3 | 4>);
90+
expectType<readonly [string, number, boolean?, ...number[]]>({} as SetRequired<readonly [string, number?, boolean?, ...number[]], 10 | 1>);
91+
92+
// Marks all keys as required, if `Keys` is `any`.
93+
expectType<[string, number, boolean]>({} as SetRequired<[string?, number?, boolean?], any>);
94+
expectType<[string, number, boolean, ...number[]]>({} as SetRequired<[string, number?, boolean?, ...number[]], any>);
95+
expectType<readonly [string, number, boolean, ...number[]]>({} as SetRequired<readonly [string, number, boolean, ...number[]], any>);
96+
expectType<readonly [string, number, boolean, ...number[]]>({} as SetRequired<readonly [string, number?, boolean?, ...number[]], any>);
97+
98+
// Marks all keys as required, if `Keys` is `number`.
99+
expectType<[string, number, boolean]>({} as SetRequired<[string?, number?, boolean?], number>);
100+
expectType<[string, number, boolean, ...number[]]>({} as SetRequired<[string, number?, boolean?, ...number[]], number>);
101+
expectType<readonly [string, number, boolean, ...number[]]>({} as SetRequired<readonly [string, number, boolean, ...number[]], number>);
102+
expectType<readonly [string, number, boolean, ...number[]]>({} as SetRequired<readonly [string, number?, boolean?, ...number[]], number>);
103+
104+
// Returns the array as-is, if `Keys` is `never`.
105+
expectType<[string?, number?]>({} as SetRequired<[string?, number?], never>);
106+
expectType<readonly [string?, number?, ...number[]]>({} as SetRequired<readonly [string?, number?, ...number[]], never>);
107+
108+
// Arrays where non-rest elements appear after the rest element are left unchanged, because they can never have optional elements.
109+
expectType<[...string[], string | undefined, number]>({} as SetRequired<[...string[], string | undefined, number], any>);
110+
expectType<[boolean, ...string[], string, number]>({} as SetRequired<[boolean, ...string[], string, number], any>);
111+
112+
// Preserves `| undefined`, similar to how built-in `Required` works.
113+
expectType<[string | undefined, number | undefined, boolean]>({} as SetRequired<[string | undefined, (number | undefined)?, boolean?], 0 | 1 | 2>);
114+
expectType<readonly [string | undefined, (number | undefined)?, boolean?]>({} as SetRequired<readonly [(string | undefined)?, (number | undefined)?, boolean?], 0>);
115+
116+
// Optional elements cannot appear after required ones, `Keys` leading to such situations are ignored.
117+
expectType<[string?, number?, boolean?]>({} as SetRequired<[string?, number?, boolean?], 1 | 2>); // `1` and `2` can't be required when `0` is optional
118+
expectType<[string, number, boolean?, string?, string?]>(
119+
{} as SetRequired<[string?, number?, boolean?, string?, string?], 0 | 1 | 3>, // `3` can't be required when `2` is optional
120+
);
121+
expectType<readonly [string | undefined, number?, boolean?, ...string[]]>(
122+
{} as SetRequired<readonly [string | undefined, number?, boolean?, ...string[]], 2>, // `2` can't be required when `1` is optional
123+
);
124+
125+
// Works with unions of arrays
126+
expectType<readonly [] | []>({} as SetRequired<readonly [] | [], never>);
127+
expectType<[] | readonly [(string | number)]>({} as SetRequired<[] | readonly [(string | number)?], 0>);
128+
expectType<[string] | [string, number, boolean?, ...number[]] | readonly [string, number, boolean?]>(
129+
{} as SetRequired<[string?] | [string, number?, boolean?, ...number[]] | readonly [string, number?, boolean?], 0 | 1>,
130+
);
131+
expectType<readonly [number, string] | [string, boolean, ...number[]] | readonly [string, number | undefined, boolean?, string?]>(
132+
{} as SetRequired<readonly [number, string] | [string, boolean?, ...number[]] | readonly [string, (number | undefined)?, boolean?, string?], 1 | 3>,
133+
);
134+
expectType<readonly [...number[], number] | [string, boolean, ...number[]] | readonly [string, number | undefined, boolean, string]>(
135+
{} as SetRequired<readonly [...number[], number] | [string, boolean?, ...number[]] | readonly [string, (number | undefined)?, boolean?, string?], any>,
136+
);
137+
expectType<readonly string[] | [x: number, y: number] | [string, number, ...string[]]>(
138+
{} as SetRequired<readonly string[] | [x: number, y?: number] | [string?, number?, ...string[]], number>,
139+
);
140+
141+
// Works with labelled tuples
142+
expectType<[x: string, y: number]>({} as SetRequired<[x?: string, y?: number], '0' | '1'>);
143+
expectType<readonly [x: number, y: number, z?: number]>({} as SetRequired<readonly [x?: number, y?: number, z?: number], 0 | 1>);
144+
expectType<readonly [x: number, y: number, z?: number, ...rest: number[]]>({} as SetRequired<readonly [x?: number, y?: number, z?: number, ...rest: number[]], 0 | 1>);
145+
146+
// Non tuple arrays are left unchanged
147+
expectType<string[]>({} as SetRequired<string[], number>);
148+
expectType<ReadonlyArray<string | number>>({} as SetRequired<ReadonlyArray<string | number>, number>);
149+
expectType<number[]>({} as SetRequired<[...number[]], never>);

0 commit comments

Comments
 (0)