Skip to content

Commit 34b8fad

Browse files
benzariasom-smsindresorhus
authored
Add SplitOnRestElement, ExtractRestElement, ExcludeRestElement types (#1166)
Co-authored-by: Som Shekhar Mukherjee <49264891+som-sm@users.noreply.github.com> Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 72f491f commit 34b8fad

File tree

8 files changed

+337
-0
lines changed

8 files changed

+337
-0
lines changed

index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export type {WritableKeysOf} from './source/writable-keys-of.d.ts';
111111
export type {IsWritableKeyOf} from './source/is-writable-key-of.d.ts';
112112
export type {HasWritableKeys} from './source/has-writable-keys.d.ts';
113113
export type {Spread} from './source/spread.d.ts';
114+
export type {SplitOnRestElement} from './source/split-on-rest-element.d.ts';
115+
export type {ExtractRestElement} from './source/extract-rest-element.d.ts';
116+
export type {ExcludeRestElement} from './source/exclude-rest-element.d.ts';
114117
export type {IsInteger} from './source/is-integer.d.ts';
115118
export type {IsFloat} from './source/is-float.d.ts';
116119
export type {TupleToObject} from './source/tuple-to-object.d.ts';

readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ Click the type names for complete docs.
258258
- [`UnionToTuple`](source/union-to-tuple.d.ts) - Convert a union type into an unordered tuple type of its elements.
259259
- [`TupleToObject`](source/tuple-to-object.d.ts) - Transforms a tuple into an object, mapping each tuple index to its corresponding type as a key-value pair.
260260
- [`TupleOf`](source/tuple-of.d.ts) - Creates a tuple type of the specified length with elements of the specified type.
261+
- [`SplitOnRestElement`](source/split-on-rest-element.d.ts) - Splits an array into three parts, where the first contains all elements before the rest element, the second is the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element itself, and the third contains all elements after the rest element.
262+
- [`ExtractRestElement`](source/extract-rest-element.d.ts) - Extracts the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element type from an array.
263+
- [`ExcludeRestElement`](source/exclude-rest-element.d.ts) - Creates a tuple with the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element removed.
261264

262265
### Numeric
263266

source/exclude-rest-element.d.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type {SplitOnRestElement} from './split-on-rest-element.d.ts';
2+
import type {IsArrayReadonly} from './internal/array.d.ts';
3+
import type {UnknownArray} from './unknown-array.d.ts';
4+
5+
/**
6+
Creates a tuple with the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element removed.
7+
8+
@example
9+
```
10+
import type {ExcludeRestElement} from 'type-fest';
11+
12+
type T1 = ExcludeRestElement<[number, ...string[], string, 'foo']>;
13+
//=> [number, string, 'foo']
14+
15+
type T2 = ExcludeRestElement<[...boolean[], string]>;
16+
//=> [string]
17+
18+
type T3 = ExcludeRestElement<[...'foo'[], true]>;
19+
//=> [true]
20+
21+
type T4 = ExcludeRestElement<[number, string]>;
22+
//=> [number, string]
23+
```
24+
25+
@see ExtractRestElement, SplitOnRestElement
26+
@category Array
27+
*/
28+
export type ExcludeRestElement<Array_ extends UnknownArray> =
29+
SplitOnRestElement<Array_> extends infer Result
30+
? Result extends readonly UnknownArray[]
31+
? IsArrayReadonly<Array_> extends true
32+
? Readonly<[...Result[0], ...Result[2]]>
33+
: [...Result[0], ...Result[2]]
34+
: Result
35+
: never;
36+
37+
export {};

source/extract-rest-element.d.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type {SplitOnRestElement} from './split-on-rest-element.d.ts';
2+
import type {UnknownArray} from './unknown-array.d.ts';
3+
4+
/**
5+
Extracts the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element type from an array.
6+
7+
@example
8+
```
9+
import type {ExtractRestElement} from 'type-fest';
10+
11+
type T1 = ExtractRestElement<[number, ...string[], string, 'foo']>;
12+
//=> string
13+
14+
type T2 = ExtractRestElement<[...boolean[], string]>;
15+
//=> boolean
16+
17+
type T3 = ExtractRestElement<[...'foo'[], true]>;
18+
//=> 'foo'
19+
20+
type T4 = ExtractRestElement<[number, string]>;
21+
//=> never
22+
```
23+
24+
@see ExcludeRestElement, SplitOnRestElement
25+
@category Array
26+
*/
27+
export type ExtractRestElement<T extends UnknownArray> = SplitOnRestElement<T>[1][number];
28+
29+
export {};

source/split-on-rest-element.d.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type {IfNotAnyOrNever, IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts';
2+
import type {ApplyDefaultOptions} from './internal/object.d.ts';
3+
import type {IsOptionalKeyOf} from './is-optional-key-of.d.ts';
4+
import type {IsArrayReadonly} from './internal/array.d.ts';
5+
import type {UnknownArray} from './unknown-array.d.ts';
6+
import type {If} from './if.d.ts';
7+
8+
/**
9+
{@link SplitOnRestElement} options.
10+
*/
11+
type SplitOnRestElementOptions = {
12+
/**
13+
Whether to preserve the optional modifier (`?`).
14+
15+
- When set to `true`, the optional modifiers are preserved as-is. For example:
16+
`SplitOnRestElement<[number, string?, ...boolean[]], {preserveOptionalModifier: true}>` returns `[[number, string?], boolean[], []]`.
17+
18+
- When set to `false`, optional elements like `T?` are transformed to `T | undefined` or simply `T` depending on the `exactOptionalPropertyTypes` compiler option. For example:
19+
- With `exactOptionalPropertyTypes` enabled: `SplitOnRestElement<[number, string?, ...boolean[]], {preserveOptionalModifier: false}>` returns `[[number, string], boolean[], []]`
20+
- And, with it disabled, the result is: `[[number, string | undefined], boolean[], []]`
21+
22+
@default true
23+
*/
24+
preserveOptionalModifier?: boolean;
25+
};
26+
27+
type DefaultSplitOnRestElementOptions = {
28+
preserveOptionalModifier: true;
29+
};
30+
31+
/**
32+
Splits an array into three parts, where the first contains all elements before the rest element, the second is the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element itself, and the third contains all elements after the rest element.
33+
34+
Note: If any of the parts are missing, then they will be represented as empty arrays. For example, `SplitOnRestElement<[string, number]>` returns `[[string, number], [], []]`, where parts corresponding to the rest element and elements after it are empty.
35+
36+
By default, the optional modifier (`?`) is preserved.
37+
See {@link SplitOnRestElementOptions `SplitOnRestElementOptions`}.
38+
39+
@example
40+
```ts
41+
import type {SplitOnRestElement} from 'type-fest';
42+
43+
type T1 = SplitOnRestElement<[number, ...string[], boolean]>;
44+
//=> [[number], string[], [boolean]]
45+
46+
type T2 = SplitOnRestElement<readonly [...boolean[], string]>;
47+
//=> readonly [[], boolean[], [string]]
48+
49+
type T3 = SplitOnRestElement<[number, string?]>;
50+
//=> [[number, string?], [], []]
51+
52+
type T4 = SplitOnRestElement<[number, string?], {preserveOptionalModifier: false}>;
53+
//=> [[number, string], [], []] or [[number, string | undefined], [], []]
54+
55+
type T5 = SplitOnRestElement<readonly [string?, ...number[]], {preserveOptionalModifier: false}>;
56+
//=> readonly [[string], number[], []] or readonly [[string | undefined], number[], []]
57+
```
58+
59+
@see ExtractRestElement
60+
@see ExcludeRestElement
61+
@category Array
62+
*/
63+
export type SplitOnRestElement<
64+
Array_ extends UnknownArray,
65+
Options extends SplitOnRestElementOptions = {},
66+
> =
67+
Array_ extends unknown // For distributing `Array_`
68+
? IfNotAnyOrNever<Array_, _SplitOnRestElement<
69+
Array_,
70+
ApplyDefaultOptions<SplitOnRestElementOptions, DefaultSplitOnRestElementOptions, Options>
71+
>> extends infer Result extends UnknownArray
72+
? If<IsArrayReadonly<Array_>, Readonly<Result>, Result>
73+
: never // Should never happen
74+
: never; // Should never happen
75+
76+
/**
77+
Deconstructs an array on its rest element and returns the split portions.
78+
*/
79+
export type _SplitOnRestElement<
80+
Array_ extends UnknownArray,
81+
Options extends Required<SplitOnRestElementOptions>,
82+
HeadAcc extends UnknownArray = [],
83+
TailAcc extends UnknownArray = [],
84+
> =
85+
keyof Array_ & `${number}` extends never
86+
// Enters this branch, if `Array_` is empty (e.g., []),
87+
// or `Array_` contains no non-rest elements preceding the rest element (e.g., `[...string[]]` or `[...string[], string]`).
88+
? Array_ extends readonly [...infer Rest, infer Last]
89+
? _SplitOnRestElement<Rest, Options, HeadAcc, [Last, ...TailAcc]> // Accumulate elements that are present after the rest element.
90+
: [HeadAcc, Array_ extends readonly [] ? [] : Array_, TailAcc] // Add the rest element between the accumulated elements.
91+
: Array_ extends readonly [(infer First)?, ...infer Rest]
92+
? _SplitOnRestElement<
93+
Rest, Options,
94+
[
95+
...HeadAcc,
96+
...IsOptionalKeyOf<Array_, '0'> extends true
97+
? Options['preserveOptionalModifier'] extends false
98+
? [If<IsExactOptionalPropertyTypesEnabled, First, First | undefined>] // Add `| undefined` for optional elements, if `exactOptionalPropertyTypes` is disabled.
99+
: [First?]
100+
: [First],
101+
],
102+
TailAcc
103+
> // Accumulate elements that are present before the rest element.
104+
: never; // Should never happen, since `[(infer First)?, ...infer Rest]` is a top-type for arrays.
105+
106+
export {};

test-d/exclude-rest-element.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {expectType} from 'tsd';
2+
import type {ExcludeRestElement} from '../index.d.ts';
3+
4+
// Basic static tuples (No rest element)
5+
expectType<ExcludeRestElement<[]>>({} as []);
6+
expectType<ExcludeRestElement<[1]>>({} as [1]);
7+
expectType<ExcludeRestElement<[1, 2, 3]>>({} as [1, 2, 3]);
8+
expectType<ExcludeRestElement<readonly ['a', 'b']>>({} as readonly ['a', 'b']);
9+
10+
// Leading rest element
11+
expectType<ExcludeRestElement<[...string[], 1]>>({} as [1]);
12+
expectType<ExcludeRestElement<[...number[], 'a', 'b']>>({} as ['a', 'b']);
13+
expectType<ExcludeRestElement<[...any[], true]>>({} as [true]);
14+
expectType<ExcludeRestElement<[...never[], 'end']>>({} as ['end']);
15+
expectType<ExcludeRestElement<[...unknown[], 2, 3]>>({} as [2, 3]);
16+
17+
// Middle rest element
18+
expectType<ExcludeRestElement<['a', ...string[], 'z']>>({} as ['a', 'z']);
19+
expectType<ExcludeRestElement<['x', ...boolean[], true]>>({} as ['x', true]);
20+
expectType<ExcludeRestElement<['x', ...any[], 'y']>>({} as ['x', 'y']);
21+
expectType<ExcludeRestElement<['x', ...readonly number[], 'y']>>({} as ['x', 'y']);
22+
23+
// Trailing rest element
24+
expectType<ExcludeRestElement<[1, 2, ...string[]]>>({} as [1, 2]);
25+
expectType<ExcludeRestElement<['foo', ...Array<'bar'>]>>({} as ['foo']);
26+
expectType<ExcludeRestElement<[number, ...number[]]>>({} as [number]);
27+
28+
// Only rest element
29+
expectType<ExcludeRestElement<string[]>>({} as []);
30+
expectType<ExcludeRestElement<readonly number[]>>({} as readonly []);
31+
expectType<ExcludeRestElement<readonly [...boolean[]]>>({} as readonly []);
32+
expectType<ExcludeRestElement<[...string[]]>>({} as []);
33+
34+
// Optional & mixed optional
35+
expectType<ExcludeRestElement<[string?, boolean?, ...number[]]>>({} as [string?, boolean?]);
36+
expectType<ExcludeRestElement<[number, boolean?, ...number[]]>>({} as [number, boolean?]);
37+
expectType<ExcludeRestElement<[1?, ...string[]]>>({} as [1?]);
38+
39+
// Unions
40+
expectType<ExcludeRestElement<[1, ...string[]] | [2, ...number[]]>>({} as [1] | [2]);
41+
expectType<ExcludeRestElement<[...boolean[], 'end'] | ['start', ...string[]]>>({} as ['end'] | ['start']);
42+
43+
// Readonly
44+
expectType<ExcludeRestElement<readonly [...number[], 'done']>>({} as readonly ['done']);
45+
expectType<ExcludeRestElement<readonly [1, ...string[], 2]>>({} as readonly [1, 2]);
46+
47+
// Nested Arrays
48+
expectType<ExcludeRestElement<[[1, 2], ...number[], [3, 4]]>>({} as [[1, 2], [3, 4]]);
49+
expectType<ExcludeRestElement<[['a'], ...string[], ['z']]>>({} as [['a'], ['z']]);
50+
51+
// Edge: `never` / `any`
52+
expectType<ExcludeRestElement<any>>({} as any);
53+
expectType<ExcludeRestElement<never>>({} as never);

test-d/extract-rest-element.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {expectType} from 'tsd';
2+
import type {ExtractRestElement} from '../index.d.ts';
3+
4+
// Leading rest element
5+
expectType<ExtractRestElement<[...string[], 1]>>({} as string);
6+
expectType<ExtractRestElement<[...number[], 'a', 'b']>>({} as number);
7+
expectType<ExtractRestElement<[...any[], true]>>({} as any);
8+
expectType<ExtractRestElement<[...never[], 'end']>>({} as never);
9+
expectType<ExtractRestElement<[...unknown[], 2, 3]>>({} as unknown);
10+
11+
// Middle rest element
12+
expectType<ExtractRestElement<['a', ...string[], 'z']>>({} as string);
13+
expectType<ExtractRestElement<['x', ...boolean[], true]>>({} as boolean);
14+
expectType<ExtractRestElement<['x', ...any[], 'y']>>({} as any);
15+
16+
// Trailing rest element
17+
expectType<ExtractRestElement<[1, 2, ...string[]]>>({} as string);
18+
expectType<ExtractRestElement<['foo', ...Array<'bar'>]>>({} as 'bar');
19+
expectType<ExtractRestElement<[number, ...number[]]>>({} as number);
20+
21+
// Rest element only
22+
expectType<ExtractRestElement<string[]>>({} as string);
23+
expectType<ExtractRestElement<readonly number[]>>({} as number);
24+
expectType<ExtractRestElement<readonly [...boolean[]]>>({} as boolean);
25+
expectType<ExtractRestElement<[...string[]]>>({} as string);
26+
27+
// Optional
28+
expectType<ExtractRestElement<[string?, boolean?, ...number[]]>>({} as number);
29+
expectType<ExtractRestElement<[number, boolean?, ...number[]]>>({} as number);
30+
expectType<ExtractRestElement<[1?, ...string[]]>>({} as string);
31+
32+
// No rest element
33+
expectType<ExtractRestElement<[1, 2, 3]>>({} as never);
34+
expectType<ExtractRestElement<readonly ['a', 'b']>>({} as never);
35+
expectType<ExtractRestElement<[]>>({} as never);
36+
37+
// Union
38+
expectType<ExtractRestElement<[1, ...string[]] | [2, ...number[]]>>({} as string | number);
39+
expectType<ExtractRestElement<[...boolean[], 'end'] | ['start', ...string[]]>>({} as boolean | string);
40+
41+
// Readonly
42+
expectType<ExtractRestElement<readonly [...number[], 'done']>>({} as number);
43+
expectType<ExtractRestElement<readonly [1, ...string[], 2]>>({} as string);
44+
45+
// Nested arrays
46+
expectType<ExtractRestElement<[[1, 2], ...string[], [3, 4]]>>({} as string);
47+
expectType<ExtractRestElement<[['a'], ...boolean[], ['z']]>>({} as boolean);
48+
49+
// Edge: `never` / `any`
50+
expectType<ExtractRestElement<any>>({} as any);
51+
expectType<ExtractRestElement<never>>({} as never);

test-d/split-on-rest-element.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {expectType} from 'tsd';
2+
import type {SplitOnRestElement} from '../index.d.ts';
3+
4+
// Fixed tuples (No rest element)
5+
expectType<SplitOnRestElement<[]>>({} as [[], [], []]);
6+
expectType<SplitOnRestElement<[1]>>({} as [[1], [], []]);
7+
expectType<SplitOnRestElement<[1, 2, 3]>>({} as [[1, 2, 3], [], []]);
8+
expectType<SplitOnRestElement<readonly ['a', 'b', 'c']>>({} as readonly [['a', 'b', 'c'], [], []]);
9+
10+
// Rest elements (true variadic)
11+
expectType<SplitOnRestElement<[1, ...number[]]>>({} as [[1], number[], []]);
12+
expectType<SplitOnRestElement<[...string[]]>>({} as [[], string[], []]);
13+
expectType<SplitOnRestElement<[...never[]]>>({} as [[], never[], []]);
14+
expectType<SplitOnRestElement<[1, ...number[], 2]>>({} as [[1], number[], [2]]);
15+
expectType<SplitOnRestElement<['a', ...string[], 'b']>>({} as [['a'], string[], ['b']]);
16+
expectType<SplitOnRestElement<[...boolean[], string]>>({} as [[], boolean[], [string]]);
17+
expectType<SplitOnRestElement<[...string[], 'x', 'y']>>({} as [[], string[], ['x', 'y']]);
18+
expectType<SplitOnRestElement<[...never[], 1]>>({} as [[], never[], [1]]);
19+
expectType<SplitOnRestElement<[undefined, ...boolean[], null]>>({} as [[undefined], boolean[], [null]]);
20+
expectType<SplitOnRestElement<[void, ...never[], 1]>>({} as [[void], never[], [1]]);
21+
expectType<SplitOnRestElement<[null, ...any[], null]>>({} as [[null], any[], [null]]);
22+
expectType<SplitOnRestElement<[...Array<{id: string}>, number]>>({} as [[], Array<{id: string}>, [number]]);
23+
expectType<SplitOnRestElement<[1, ...Array<readonly [string, number]>, 2]>>({} as [[1], Array<readonly [string, number]>, [2]]);
24+
25+
// Generic arrays
26+
expectType<SplitOnRestElement<string[]>>({} as [[], string[], []]);
27+
expectType<SplitOnRestElement<number[]>>({} as [[], number[], []]);
28+
expectType<SplitOnRestElement<unknown[]>>({} as [[], unknown[], []]);
29+
expectType<SplitOnRestElement<any[]>>({} as [[], any[], []]);
30+
31+
// Unions
32+
expectType<SplitOnRestElement<[...Array<string | number>, boolean]>>({} as [[], Array<string | number>, [boolean]]);
33+
expectType<SplitOnRestElement<[1, 2] | [3, 4]>>({} as [[1, 2], [], []] | [[3, 4], [], []]);
34+
expectType<SplitOnRestElement<[1, ...number[]] | ['foo', ...string[]]>>({} as [[1], number[], []] | [['foo'], string[], []]);
35+
36+
// Preserve optional
37+
expectType<SplitOnRestElement<[0, 1?, 2?, ...never[]]>>({} as [[0, 1?, 2?], never[], []]);
38+
expectType<SplitOnRestElement<[number?, ...string[]]>>({} as [[number?], string[], []]);
39+
expectType<SplitOnRestElement<[number, boolean?, ...string[]]>>({} as [[number, boolean?], string[], []]);
40+
41+
// Remove optional
42+
expectType<SplitOnRestElement<[0, 1?, 2?, ...never[]], {preserveOptionalModifier: false}>>({} as [[0, 1, 2], never[], []]);
43+
expectType<SplitOnRestElement<[number?, ...string[]], {preserveOptionalModifier: false}>>({} as [[number], string[], []]);
44+
expectType<SplitOnRestElement<[number, boolean?, ...string[]], {preserveOptionalModifier: false}>>({} as [[number, boolean], string[], []]);
45+
46+
// Readonly
47+
expectType<SplitOnRestElement<readonly []>>({} as readonly [[], [], []]);
48+
expectType<SplitOnRestElement<readonly [number] | [string]>>({} as readonly [[number], [], []] | [[string], [], []]);
49+
expectType<SplitOnRestElement<readonly [...number[], 2]>>({} as readonly [[], number[], [2]]);
50+
expectType<SplitOnRestElement<readonly [1, ...string[], 2] | readonly ['foo'?, ...string[]]>>({} as readonly [[1], string[], [2]] | readonly [['foo'?], string[], []]);
51+
expectType<SplitOnRestElement<readonly [1, 2, 3]>>({} as readonly [[1, 2, 3], [], []]);
52+
53+
// Edge: `never` / `any`
54+
expectType<SplitOnRestElement<any>>({} as any);
55+
expectType<SplitOnRestElement<never>>({} as never);

0 commit comments

Comments
 (0)