diff --git a/index.d.ts b/index.d.ts index aa48c653d..b4cc18eb4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -111,6 +111,9 @@ export type {WritableKeysOf} from './source/writable-keys-of.d.ts'; export type {IsWritableKeyOf} from './source/is-writable-key-of.d.ts'; export type {HasWritableKeys} from './source/has-writable-keys.d.ts'; export type {Spread} from './source/spread.d.ts'; +export type {SplitOnRestElement} from './source/split-on-rest-element.d.ts'; +export type {ExtractRestElement} from './source/extract-rest-element.d.ts'; +export type {ExcludeRestElement} from './source/exclude-rest-element.d.ts'; export type {IsInteger} from './source/is-integer.d.ts'; export type {IsFloat} from './source/is-float.d.ts'; export type {TupleToObject} from './source/tuple-to-object.d.ts'; diff --git a/readme.md b/readme.md index c3006aff5..52b4712ae 100644 --- a/readme.md +++ b/readme.md @@ -258,6 +258,9 @@ Click the type names for complete docs. - [`UnionToTuple`](source/union-to-tuple.d.ts) - Convert a union type into an unordered tuple type of its elements. - [`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. - [`TupleOf`](source/tuple-of.d.ts) - Creates a tuple type of the specified length with elements of the specified type. +- [`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. +- [`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. +- [`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. ### Numeric diff --git a/source/exclude-rest-element.d.ts b/source/exclude-rest-element.d.ts new file mode 100644 index 000000000..5f96ab407 --- /dev/null +++ b/source/exclude-rest-element.d.ts @@ -0,0 +1,37 @@ +import type {SplitOnRestElement} from './split-on-rest-element.d.ts'; +import type {IsArrayReadonly} from './internal/array.d.ts'; +import type {UnknownArray} from './unknown-array.d.ts'; + +/** +Creates a tuple with the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element removed. + +@example +``` +import type {ExcludeRestElement} from 'type-fest'; + +type T1 = ExcludeRestElement<[number, ...string[], string, 'foo']>; +//=> [number, string, 'foo'] + +type T2 = ExcludeRestElement<[...boolean[], string]>; +//=> [string] + +type T3 = ExcludeRestElement<[...'foo'[], true]>; +//=> [true] + +type T4 = ExcludeRestElement<[number, string]>; +//=> [number, string] +``` + +@see ExtractRestElement, SplitOnRestElement +@category Array +*/ +export type ExcludeRestElement = + SplitOnRestElement extends infer Result + ? Result extends readonly UnknownArray[] + ? IsArrayReadonly extends true + ? Readonly<[...Result[0], ...Result[2]]> + : [...Result[0], ...Result[2]] + : Result + : never; + +export {}; diff --git a/source/extract-rest-element.d.ts b/source/extract-rest-element.d.ts new file mode 100644 index 000000000..4099ddf40 --- /dev/null +++ b/source/extract-rest-element.d.ts @@ -0,0 +1,29 @@ +import type {SplitOnRestElement} from './split-on-rest-element.d.ts'; +import type {UnknownArray} from './unknown-array.d.ts'; + +/** +Extracts the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element type from an array. + +@example +``` +import type {ExtractRestElement} from 'type-fest'; + +type T1 = ExtractRestElement<[number, ...string[], string, 'foo']>; +//=> string + +type T2 = ExtractRestElement<[...boolean[], string]>; +//=> boolean + +type T3 = ExtractRestElement<[...'foo'[], true]>; +//=> 'foo' + +type T4 = ExtractRestElement<[number, string]>; +//=> never +``` + +@see ExcludeRestElement, SplitOnRestElement +@category Array +*/ +export type ExtractRestElement = SplitOnRestElement[1][number]; + +export {}; diff --git a/source/split-on-rest-element.d.ts b/source/split-on-rest-element.d.ts new file mode 100644 index 000000000..102ab55f6 --- /dev/null +++ b/source/split-on-rest-element.d.ts @@ -0,0 +1,106 @@ +import type {IfNotAnyOrNever, IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts'; +import type {ApplyDefaultOptions} from './internal/object.d.ts'; +import type {IsOptionalKeyOf} from './is-optional-key-of.d.ts'; +import type {IsArrayReadonly} from './internal/array.d.ts'; +import type {UnknownArray} from './unknown-array.d.ts'; +import type {If} from './if.d.ts'; + +/** +{@link SplitOnRestElement} options. +*/ +type SplitOnRestElementOptions = { + /** + Whether to preserve the optional modifier (`?`). + + - When set to `true`, the optional modifiers are preserved as-is. For example: + `SplitOnRestElement<[number, string?, ...boolean[]], {preserveOptionalModifier: true}>` returns `[[number, string?], boolean[], []]`. + + - When set to `false`, optional elements like `T?` are transformed to `T | undefined` or simply `T` depending on the `exactOptionalPropertyTypes` compiler option. For example: + - With `exactOptionalPropertyTypes` enabled: `SplitOnRestElement<[number, string?, ...boolean[]], {preserveOptionalModifier: false}>` returns `[[number, string], boolean[], []]` + - And, with it disabled, the result is: `[[number, string | undefined], boolean[], []]` + + @default true + */ + preserveOptionalModifier?: boolean; +}; + +type DefaultSplitOnRestElementOptions = { + preserveOptionalModifier: true; +}; + +/** +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. + +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. + +By default, the optional modifier (`?`) is preserved. +See {@link SplitOnRestElementOptions `SplitOnRestElementOptions`}. + +@example +```ts +import type {SplitOnRestElement} from 'type-fest'; + +type T1 = SplitOnRestElement<[number, ...string[], boolean]>; +//=> [[number], string[], [boolean]] + +type T2 = SplitOnRestElement; +//=> readonly [[], boolean[], [string]] + +type T3 = SplitOnRestElement<[number, string?]>; +//=> [[number, string?], [], []] + +type T4 = SplitOnRestElement<[number, string?], {preserveOptionalModifier: false}>; +//=> [[number, string], [], []] or [[number, string | undefined], [], []] + +type T5 = SplitOnRestElement; +//=> readonly [[string], number[], []] or readonly [[string | undefined], number[], []] +``` + +@see ExtractRestElement +@see ExcludeRestElement +@category Array +*/ +export type SplitOnRestElement< + Array_ extends UnknownArray, + Options extends SplitOnRestElementOptions = {}, +> = + Array_ extends unknown // For distributing `Array_` + ? IfNotAnyOrNever + >> extends infer Result extends UnknownArray + ? If, Readonly, Result> + : never // Should never happen + : never; // Should never happen + +/** +Deconstructs an array on its rest element and returns the split portions. +*/ +export type _SplitOnRestElement< + Array_ extends UnknownArray, + Options extends Required, + HeadAcc extends UnknownArray = [], + TailAcc extends UnknownArray = [], +> = + keyof Array_ & `${number}` extends never + // Enters this branch, if `Array_` is empty (e.g., []), + // or `Array_` contains no non-rest elements preceding the rest element (e.g., `[...string[]]` or `[...string[], string]`). + ? Array_ extends readonly [...infer Rest, infer Last] + ? _SplitOnRestElement // Accumulate elements that are present after the rest element. + : [HeadAcc, Array_ extends readonly [] ? [] : Array_, TailAcc] // Add the rest element between the accumulated elements. + : Array_ extends readonly [(infer First)?, ...infer Rest] + ? _SplitOnRestElement< + Rest, Options, + [ + ...HeadAcc, + ...IsOptionalKeyOf extends true + ? Options['preserveOptionalModifier'] extends false + ? [If] // Add `| undefined` for optional elements, if `exactOptionalPropertyTypes` is disabled. + : [First?] + : [First], + ], + TailAcc + > // Accumulate elements that are present before the rest element. + : never; // Should never happen, since `[(infer First)?, ...infer Rest]` is a top-type for arrays. + +export {}; diff --git a/test-d/exclude-rest-element.ts b/test-d/exclude-rest-element.ts new file mode 100644 index 000000000..6a4fba21c --- /dev/null +++ b/test-d/exclude-rest-element.ts @@ -0,0 +1,53 @@ +import {expectType} from 'tsd'; +import type {ExcludeRestElement} from '../index.d.ts'; + +// Basic static tuples (No rest element) +expectType>({} as []); +expectType>({} as [1]); +expectType>({} as [1, 2, 3]); +expectType>({} as readonly ['a', 'b']); + +// Leading rest element +expectType>({} as [1]); +expectType>({} as ['a', 'b']); +expectType>({} as [true]); +expectType>({} as ['end']); +expectType>({} as [2, 3]); + +// Middle rest element +expectType>({} as ['a', 'z']); +expectType>({} as ['x', true]); +expectType>({} as ['x', 'y']); +expectType>({} as ['x', 'y']); + +// Trailing rest element +expectType>({} as [1, 2]); +expectType]>>({} as ['foo']); +expectType>({} as [number]); + +// Only rest element +expectType>({} as []); +expectType>({} as readonly []); +expectType>({} as readonly []); +expectType>({} as []); + +// Optional & mixed optional +expectType>({} as [string?, boolean?]); +expectType>({} as [number, boolean?]); +expectType>({} as [1?]); + +// Unions +expectType>({} as [1] | [2]); +expectType>({} as ['end'] | ['start']); + +// Readonly +expectType>({} as readonly ['done']); +expectType>({} as readonly [1, 2]); + +// Nested Arrays +expectType>({} as [[1, 2], [3, 4]]); +expectType>({} as [['a'], ['z']]); + +// Edge: `never` / `any` +expectType>({} as any); +expectType>({} as never); diff --git a/test-d/extract-rest-element.ts b/test-d/extract-rest-element.ts new file mode 100644 index 000000000..4927ed543 --- /dev/null +++ b/test-d/extract-rest-element.ts @@ -0,0 +1,51 @@ +import {expectType} from 'tsd'; +import type {ExtractRestElement} from '../index.d.ts'; + +// Leading rest element +expectType>({} as string); +expectType>({} as number); +expectType>({} as any); +expectType>({} as never); +expectType>({} as unknown); + +// Middle rest element +expectType>({} as string); +expectType>({} as boolean); +expectType>({} as any); + +// Trailing rest element +expectType>({} as string); +expectType]>>({} as 'bar'); +expectType>({} as number); + +// Rest element only +expectType>({} as string); +expectType>({} as number); +expectType>({} as boolean); +expectType>({} as string); + +// Optional +expectType>({} as number); +expectType>({} as number); +expectType>({} as string); + +// No rest element +expectType>({} as never); +expectType>({} as never); +expectType>({} as never); + +// Union +expectType>({} as string | number); +expectType>({} as boolean | string); + +// Readonly +expectType>({} as number); +expectType>({} as string); + +// Nested arrays +expectType>({} as string); +expectType>({} as boolean); + +// Edge: `never` / `any` +expectType>({} as any); +expectType>({} as never); diff --git a/test-d/split-on-rest-element.ts b/test-d/split-on-rest-element.ts new file mode 100644 index 000000000..429390d62 --- /dev/null +++ b/test-d/split-on-rest-element.ts @@ -0,0 +1,55 @@ +import {expectType} from 'tsd'; +import type {SplitOnRestElement} from '../index.d.ts'; + +// Fixed tuples (No rest element) +expectType>({} as [[], [], []]); +expectType>({} as [[1], [], []]); +expectType>({} as [[1, 2, 3], [], []]); +expectType>({} as readonly [['a', 'b', 'c'], [], []]); + +// Rest elements (true variadic) +expectType>({} as [[1], number[], []]); +expectType>({} as [[], string[], []]); +expectType>({} as [[], never[], []]); +expectType>({} as [[1], number[], [2]]); +expectType>({} as [['a'], string[], ['b']]); +expectType>({} as [[], boolean[], [string]]); +expectType>({} as [[], string[], ['x', 'y']]); +expectType>({} as [[], never[], [1]]); +expectType>({} as [[undefined], boolean[], [null]]); +expectType>({} as [[void], never[], [1]]); +expectType>({} as [[null], any[], [null]]); +expectType, number]>>({} as [[], Array<{id: string}>, [number]]); +expectType, 2]>>({} as [[1], Array, [2]]); + +// Generic arrays +expectType>({} as [[], string[], []]); +expectType>({} as [[], number[], []]); +expectType>({} as [[], unknown[], []]); +expectType>({} as [[], any[], []]); + +// Unions +expectType, boolean]>>({} as [[], Array, [boolean]]); +expectType>({} as [[1, 2], [], []] | [[3, 4], [], []]); +expectType>({} as [[1], number[], []] | [['foo'], string[], []]); + +// Preserve optional +expectType>({} as [[0, 1?, 2?], never[], []]); +expectType>({} as [[number?], string[], []]); +expectType>({} as [[number, boolean?], string[], []]); + +// Remove optional +expectType>({} as [[0, 1, 2], never[], []]); +expectType>({} as [[number], string[], []]); +expectType>({} as [[number, boolean], string[], []]); + +// Readonly +expectType>({} as readonly [[], [], []]); +expectType>({} as readonly [[number], [], []] | [[string], [], []]); +expectType>({} as readonly [[], number[], [2]]); +expectType>({} as readonly [[1], string[], [2]] | readonly [['foo'?], string[], []]); +expectType>({} as readonly [[1, 2, 3], [], []]); + +// Edge: `never` / `any` +expectType>({} as any); +expectType>({} as never);