Skip to content

Add ArrayFlat type #1085

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export type {ArrayValues} from './source/array-values';
export type {ArraySlice} from './source/array-slice';
export type {ArraySplice} from './source/array-splice';
export type {ArrayTail} from './source/array-tail';
export type {ArrayFlat} from './source/array-flat';
export type {SetFieldType} from './source/set-field-type';
export type {Paths} from './source/paths';
export type {AllUnionFields} from './source/all-union-fields';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ type ShouldBeNever = IfAny<'not any', 'not never', 'never'>;
- [`Includes`](source/includes.d.ts) - Returns a boolean for whether the given array includes the given item.
- [`Join`](source/join.d.ts) - Join an array of strings and/or numbers using the given string as a delimiter.
- [`ArraySlice`](source/array-slice.d.ts) - Returns an array slice of a given range, just like `Array#slice()`.
- [`ArrayFlat`](source/array-flat.d.ts) - Creates a new array type by flattening an array to a specified depth, just like `Array#flat()`.
- [`LastArrayElement`](source/last-array-element.d.ts) - Extracts the type of the last element of an array.
- [`FixedLengthArray`](source/fixed-length-array.d.ts) - Create a type that represents an array of the given type and length.
- [`MultidimensionalArray`](source/multidimensional-array.d.ts) - Create a type that represents a multidimensional array of the given type and dimensions.
Expand Down
256 changes: 256 additions & 0 deletions source/array-flat.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import type {And} from './and';
import type {ArrayLength, ExactOptionalPropertyTypesEnable, IsLeadingSpreadArray, IsTrailingSpreadArray, Not, OptionalPartOfArray, RequiredPartOfArray, StaticPartOfArray, StaticPartOfLeadingSpreadArray, VariablePartOfArray, VariablePartOfLeadingSpreadArray} from './internal';
import type {IsEqual} from './is-equal';
import type {Or} from './or';
import type {Subtract} from './subtract';
import type {UnknownArray} from './unknown-array';

declare const RepeatSymbol: unique symbol;

/**
Return true if the number is 0
*/
type IsZero<T extends number> = [T] extends [0] ? true : false;

/**
* Options for the `ArrayFlat` type.
*/
type ArrayFlatOptions = {
/**
* The number of times to repeat the array items when flattening an non-fixed length array.
*
* @example
* ```
* type FlatArr0 = ArrayFlat<Array<number, string>, 1, { maxRepeat: 3 }>;
* //=> type FlatArr0 =
* []
* | [number, string]
* | [number, string, number, string]
* | [number, string, number, string, number, string];
* ```
*
* @default 5
*/
maxRepeat: number;
};

type DefaultArrayFlatOptions = {
maxRepeat: 5;
};

/**
Creates a new array type by flattening an array to a specified depth.

Use-case: Flatten an array type to a specified depth.

Like [`Array#flat()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat) but for types.

@example
```
import type {ArrayFlat, PositiveInfinity} from 'type-fest';

type FlatArr0 = ArrayFlat<[[0, 1], [2, 3], [4, 5]]>;
//=> type FlatArr0 = [0, 1, 2, 3, 4, 5];

// Flatten to depth
type Arr1 = [[0, [1, [2, [3, [4, [5]]]]]]];
type FlatArr1 = ArrayFlat<Arr1, 1>;
//=> type FlatArr1 = [0, [1, [2, [3, [4, [5]]]]]];

type FlatArr2 = ArrayFlat<Arr1, 3>;
//=> type FlatArr2 = [0, 1, 2, [3, [4, [5]]]];

// Flatten to depth Infinity
type FlatArr3 = ArrayFlat<Arr1, PositiveInfinity>;
//=> type FlatArr3 = [0, 1, 2, 3, 4, 5];
```

@category Array
*/
export type ArrayFlat<T, Depth extends number = 1, Options extends ArrayFlatOptions = DefaultArrayFlatOptions> =
DoRepeatArrayItem<InternalArrayFlat<T, Depth, Options>, Options['maxRepeat']>;

// Internal implementation
type InternalArrayFlat<
T,
Depth extends number = 1,
Options extends ArrayFlatOptions = DefaultArrayFlatOptions,
Result extends UnknownArray = [],
> =
T extends UnknownArray
? Or<IsZero<ArrayLength<T>>, IsZero<Depth>> extends true
? [...Result, ...T]
: number extends T['length']
// Handle non-fixed length arrays
? InternalNonFixedLengthArrayFlat<T, Depth, Options, Result>
// Handle fixed length arrays
: InternalFixedLengthArrayFlat<T, Depth, Options, Result>
: [];

// Handle non-fixed length arrays
type InternalNonFixedLengthArrayFlat<
T,
Depth extends number = 1,
Options extends ArrayFlatOptions = DefaultArrayFlatOptions,
Result extends UnknownArray = [],
> =
T extends UnknownArray
? Or<IsZero<ArrayLength<T>>, IsZero<Depth>> extends true
? [...Result, ...T]
: IsTrailingSpreadArray<T> extends true
// Handle trailing spread array
? [StaticPartOfArray<T>, VariablePartOfArray<T>] extends [infer StaticPart, infer VariablePart]
? InternalFixedLengthArrayFlat<StaticPart, Depth, Options> extends infer StaticPartResult extends UnknownArray
? [StaticPartResult, InternalNonFixedLengthArrayFlat<VariablePart, Depth, Options>] extends
[infer Result1 extends UnknownArray, infer Result2 extends UnknownArray]
? [...Result, ...Result1, ...Result2]
: never
: never
: never // Never happens
// Handle leading spread array
: IsLeadingSpreadArray<T> extends true
? [VariablePartOfLeadingSpreadArray<T>, StaticPartOfLeadingSpreadArray<T>] extends [infer VariablePart, infer StaticPart]
? [InternalNonFixedLengthArrayFlat<VariablePart, Depth, Options>, InternalFixedLengthArrayFlat<StaticPart, Depth, Options>] extends
[infer Result1 extends UnknownArray, infer Result2 extends UnknownArray]
? [...Result, ...Result1, ...Result2]
: never
: never // Never happens
// Handle non-spread and non-fixed-length array
: [
T[number] extends UnknownArray
? InternalArrayFlat<T[number], Subtract<Depth, 1>, Options, Result>
: [T[number]],
] extends [infer Item extends UnknownArray]
? Item extends [{[RepeatSymbol]: unknown}]
? Item
: [{[RepeatSymbol]: Item}]
: never // Never happens
: T;

// Handle fixed length arrays
type InternalFixedLengthArrayFlat<
T,
Depth extends number = 1,
Options extends ArrayFlatOptions = DefaultArrayFlatOptions,
Result extends UnknownArray = [],
> =
T extends UnknownArray
? Or<IsZero<ArrayLength<T>>, IsZero<Depth>> extends true
? [...Result, ...T]
: T extends readonly [infer ArrayItem, ...infer Last]
? [ArrayItem] extends [UnknownArray]
? number extends ArrayLength<ArrayItem>
? InternalNonFixedLengthArrayFlat<ArrayItem, Depth, Options> extends infer Item extends UnknownArray
? InternalFixedLengthArrayFlat<Last, Depth, Options, [...Result, ...Item]>
: never // Never happens, just for fixed ts error TS2589: Type instantiation is excessively deep and possibly infinite.
: [RequiredPartOfArray<ArrayItem>, OptionalPartOfArray<ArrayItem>] extends [infer RequiredPart, infer OptionalPart]
? InternalArrayFlat<
Last,
Depth,
Options,
[InternalArrayFlat<RequiredPart, Subtract<Depth, 1>, Options>, (InternalArrayFlat<OptionalPart, Subtract<Depth, 1>, Options> | [])] extends
[infer Result1 extends UnknownArray, infer Result2 extends UnknownArray]
? [...Result, ...Result1, ...Result2]
: never
>
: never // Never happens
: InternalInnerFixedLengthArrayFlat<Last, Depth, Options, [...Result, ArrayItem]>
: [...Result, ...T]
: [];

// Handle fixed length arrays
type InternalInnerFixedLengthArrayFlat<
T,
Depth extends number = 1,
Options extends ArrayFlatOptions = DefaultArrayFlatOptions,
Result extends UnknownArray = [],
> =
[T] extends [UnknownArray]
? Or<IsZero<ArrayLength<T>>, IsZero<Depth>> extends true
? [...Result, ...T]
: [RequiredPartOfArray<T>, OptionalPartOfArray<T>] extends [infer RequiredPart, infer OptionalPart]
? [InternalArrayFlat<RequiredPart, Depth, Options>, (InternalArrayFlat<OptionalPart, Depth, Options> | [])] extends
[infer Result1 extends UnknownArray, infer Result2 extends UnknownArray]
? [...Result, ...Result1, ...Result2]
: never // Never happens, just for fixed ts error TS2589: Type instantiation is excessively deep and possibly infinite.
: never // Never happens
: [];

/**
* Replaces items with the RepeatSymbol flag to the true result.
*/
type DoRepeatArrayItem<T, RepeatNumber extends number, hasSpreadArray extends boolean = false> =
T extends [infer _Item, ...infer Last]
? [_Item] extends [{[RepeatSymbol]: infer Item extends UnknownArray}]
? IsZero<Item['length']> extends true
? [...DoRepeatArrayItem<Last, RepeatNumber>]
: Item extends unknown
? Item['length'] extends 1
// If the item is a single element array, we can build [...Array<Item[number]>], but if already has spread
// array before, we should build [...Array<'SomeSpreadArrayBefore'>, Item[number], Item[number], Item[number], ...]
? [
...(
hasSpreadArray extends true
? BuildRepeatedUnionArray<Item, RepeatNumber, Not<hasSpreadArray>> extends infer Result extends UnknownArray
? [...Result]
: never // Never happens, just for fixed ts error TS2589: Type instantiation is excessively deep and possibly infinite.
: Array<Item[number]>
)
, ...DoRepeatArrayItem<Last, RepeatNumber, true>,
]
// If the item is not a single element array, we only can build by repeating the item, like:
// ArrayFlat<Array<[1, 2]>> => [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, ...]
: [
...(
BuildRepeatedUnionArray<Item, RepeatNumber, Not<hasSpreadArray>> extends infer Result extends UnknownArray
? [...Result]
: never // Never happens, just for fixed ts error TS2589: Type instantiation is excessively deep and possibly infinite.
)
, ...DoRepeatArrayItem<Last, RepeatNumber, false>, // eslint-disable-line @typescript-eslint/no-unnecessary-type-arguments
]
: never // Never happens
: [_Item, ...DoRepeatArrayItem<Last, RepeatNumber>]
: T;

/**
* Builds a union that lists all the possible combinations of the given array items and repeat times.
*
* @example
* ```
* type A = BuildRepeatedUnionArray<[number, string?], 2, true>;
* //=> type A =
* []
* | number[]
* | [number]
* | [number, string]
* | [number, number]
* | [number, string, number]
* | [number, number, string]
* | [number, string, number, string]
* ```
*/
type BuildRepeatedUnionArray<T extends UnknownArray, RepeatNumber extends number, CanSpread extends boolean = false, R extends unknown[] = []> =
RepeatNumber extends 0
? R
: [RequiredPartOfArray<T>, OptionalPartOfArray<T>] extends [infer RequiredPart extends UnknownArray, infer OptionalPart extends UnknownArray]
? ExactOptionalPropertyTypesEnable extends true
? R
| [...RequiredPart]
| (And<IsEqual<RequiredPart['length'], 1>, CanSpread> extends true
? [...Array<RequiredPart[number]>]
: never)
| BuildRepeatedUnionArray<
T,
Subtract<RepeatNumber, 1>,
CanSpread,
[
...R,
...(
ExactOptionalPropertyTypesEnable extends true
? [...RequiredPart, ...(IsZero<ArrayLength<OptionalPart>> extends true ? [] : [Exclude<OptionalPart[number], undefined>] | [])]
: T
),
]
>
: never
: never;
70 changes: 70 additions & 0 deletions source/internal/array.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@ It creates a type-safe way to access the element type of `unknown` type.
*/
export type ArrayElement<T> = T extends readonly unknown[] ? T[0] : never;

/**
Returns the required part of the given array.

@example
```
type A = [string, number, boolean?];
type B = RequiredPartOfArray<A>;
//=> [string, number]
```
*/
export type RequiredPartOfArray<T extends UnknownArray> =
T extends readonly [infer U, ...infer V]
? [U, ...RequiredPartOfArray<V>]
: [];

/**
Returns the optional part of the given array.

@example
```
type A = [string, number, boolean?];
type B = OptionalPartOfArray<A>;
//=> [boolean?]
```
*/
export type OptionalPartOfArray<T extends UnknownArray> =
T extends readonly [...RequiredPartOfArray<T>, ...infer U]
? U
: [];

/**
Returns the static, fixed-length portion of the given array, excluding variable-length parts.

Expand Down Expand Up @@ -66,6 +96,46 @@ export type VariablePartOfArray<T extends UnknownArray> =
: []
: never; // Should never happen

/**
Returns if the given array is a leading spread array.
*/
export type IsLeadingSpreadArray<T extends UnknownArray> =
T extends [...infer U, infer V] ? true : false;

/**
Returns if the given array is a trailing spread array.
*/
export type IsTrailingSpreadArray<T extends UnknownArray> =
T extends [infer U, ...infer V] ? true : false;

/**
Returns the static, fixed-length portion of the given leading spread array.
@example
```
type A = [...string[], number, boolean];
type B = StaticPartOfLeadingSpreadArray<A>;
//=> [number, boolean]
```
*/
type StaticPartOfLeadingSpreadArray<T extends UnknownArray, Result extends UnknownArray = []> =
T extends [...infer U, infer V]
? StaticPartOfLeadingSpreadArray<U, [V, ...Result]>
: Result;

/**
Returns the variable, non-fixed-length portion of the given leading spread array.
@example
```
type A = [...string[], number, boolean];
type B = VariablePartOfLeadingSpreadArray<A>;
//=> string[]
```
*/
export type VariablePartOfLeadingSpreadArray<T extends UnknownArray> =
T extends [...infer U, ...StaticPartOfLeadingSpreadArray<T>]
? U
: never;

/**
Set the given array to readonly if `IsReadonly` is `true`, otherwise set the given array to normal, then return the result.

Expand Down
5 changes: 5 additions & 0 deletions source/internal/type.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,8 @@ export type IfNotAnyOrNever<T, IfNotAnyOrNever, IfAny = any, IfNever = never> =
: IsNever<T> extends true
? IfNever
: IfNotAnyOrNever;

/**
Return the value of exactOptionalPropertyTypes option in tsconfig
*/
export type ExactOptionalPropertyTypesEnable = [(string | undefined)?] extends [string?] ? false : true;
Loading