Skip to content
Merged
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
79 changes: 66 additions & 13 deletions source/last-array-element.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import type {If} from './if.d.ts';
import type {IfNotAnyOrNever, IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts';
import type {SplitOnRestElement} from './split-on-rest-element.d.ts';
import type {UnknownArray} from './unknown-array.d.ts';

/**
Extract the type of the last element of an array.

Expand All @@ -16,21 +21,69 @@ const last2 = lastOf([true, false, 'baz', 10]);
//=> 10
```

Note: When the array ends with an optional or rest element, the last element's position becomes ambiguous. In such cases, the result is a union of the types of all elements that could potentially be the last element of the array.

@example
```
import type {LastArrayElement} from 'type-fest';

type A = LastArrayElement<[string, number?, bigint?]>;
//=> bigint | number | string

type B = LastArrayElement<[string, number, bigint?, ...boolean[]]>;
//=> boolean | bigint | number
```

Note: If empty array is a valid value for the array type, the result includes an `undefined`. This aligns with the runtime behavior of `[].at(-1)`.

@example
```
import type {LastArrayElement} from 'type-fest';

type A = LastArrayElement<[]>;
//=> undefined

// `[]` is assignable to `string[]`
type B = LastArrayElement<string[]>;
//=> string | undefined

// `[]` is assignable to `[string?, number?]`
type C = LastArrayElement<[string?, number?]>;
//=> number | string | undefined

// `[]` is assignable to [string?, number?, ...bigint[]]`
type D = LastArrayElement<[string?, number?, ...bigint[]]>;
//=> bigint | number | string | undefined
```

@category Array
@category Template literal
*/
export type LastArrayElement<Elements extends readonly unknown[], ElementBeforeTailingSpreadElement = never> =
// If the last element of an array is a spread element, the `LastArrayElement` result should be `'the type of the element before the spread element' | 'the type of the spread element'`.
Elements extends readonly []
? ElementBeforeTailingSpreadElement
: Elements extends readonly [...infer U, infer V]
? V
: Elements extends readonly [infer U, ...infer V]
// If we return `V[number] | U` directly, it would be wrong for `[[string, boolean, object, ...number[]]`.
// So we need to recurse type `V` and carry over the type of the element before the spread element.
? LastArrayElement<V, U>
: Elements extends ReadonlyArray<infer U>
? U | ElementBeforeTailingSpreadElement
: never;
export type LastArrayElement<TArray extends UnknownArray> =
IfNotAnyOrNever<TArray, {
ifNot: TArray extends UnknownArray // For distributing `TArray`
? SplitOnRestElement<TArray> extends readonly [infer BeforeRest extends UnknownArray, infer Rest extends UnknownArray, infer AfterRest extends UnknownArray]
? _LastArrayElement<BeforeRest, Rest, AfterRest>
: never
: never;
}>;

type _LastArrayElement<BeforeRest extends UnknownArray, Rest extends UnknownArray, AfterRest extends UnknownArray> =
AfterRest extends readonly [...any, infer Last] // Note there are no optional elements in `AfterRest`.
? Last // If there's a `Last` in `AfterRest`, then that's the result.
: Rest[number] | BeforeRestLastElement<BeforeRest>; // Otherwise, the result is union of the `Rest` element and the last element in `BeforeRest`.

type BeforeRestLastElement<BeforeRest extends UnknownArray, Accumulator = never> =
BeforeRest extends readonly []
? Accumulator | undefined
: BeforeRest extends readonly [...any, infer Last]
? Last | Accumulator
: BeforeRest extends readonly [...infer Rest, (infer Last)?]
? BeforeRestLastElement<
Rest,
// Add `undefined` for optional elements, if `exactOptionalPropertyTypes` is disabled.
Last | Accumulator | If<IsExactOptionalPropertyTypesEnabled, never, undefined>
>
: never;

export {};
76 changes: 59 additions & 17 deletions test-d/last-array-element.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,67 @@
import {expectType} from 'tsd';
import type {LastArrayElement} from '../index.d.ts';

declare function lastOf<V extends readonly unknown[]>(array: V): LastArrayElement<V>;
const array: ['foo', 2, 'bar'] = ['foo', 2, 'bar'];
const mixedArray: ['bar', 'foo', 2] = ['bar', 'foo', 2];
// Empty array
expectType<undefined>(undefined as LastArrayElement<[]>);
expectType<undefined>(undefined as LastArrayElement<readonly []>);

expectType<'bar'>(lastOf(array));
expectType<2>(lastOf(mixedArray));
expectType<string>(lastOf(['a', 'b', 'c']));
expectType<string | number>(lastOf(['a', 'b', 1]));
expectType<1>(lastOf(['a', 'b', 1] as const));
// Only required elements
expectType<boolean>({} as LastArrayElement<[number, string, boolean]>);
expectType<boolean>({} as LastArrayElement<readonly [number, string, boolean]>);

declare const leadingSpreadTuple: [...string[], object, number];
expectType<number>(lastOf(leadingSpreadTuple));
// Required and optional elements
expectType<2 | 3>({} as LastArrayElement<[1, 2, 3?]>);
expectType<1 | 2 | 3>({} as LastArrayElement<readonly [1, 2?, 3?]>);
expectType<number | boolean | bigint | string>(
{} as LastArrayElement<[null, string, bigint?, boolean?, number?]>,
);

declare const trailingSpreadTuple1: [string, ...number[]];
expectType<number | string>(lastOf(trailingSpreadTuple1));
// Required and rest element
expectType<3 | string>({} as LastArrayElement<readonly [1, 2, 3, ...string[]]>); // Rest element at the end
expectType<'z'>({} as LastArrayElement<[1, 2, 3, ...boolean[], 'x', 'y', 'z']>); // Rest element in the middle
expectType<string>({} as LastArrayElement<readonly [...bigint[], string]>); // Rest element at the start

declare const trailingSpreadTuple2: [string, boolean, ...number[]];
expectType<number | boolean>(lastOf(trailingSpreadTuple2));
// Only optional elements
expectType<'a' | undefined>({} as LastArrayElement<['a'?]>);
expectType<{data: string[]} | undefined>({} as LastArrayElement<readonly [{data: string[]}?]>);
expectType<string | boolean | bigint | undefined>({} as LastArrayElement<[string?, boolean?, bigint?]>);

// eslint-disable-next-line @typescript-eslint/array-type
declare const trailingSpreadTuple3: ['foo', true, ...(1 | '2')[]];
expectType<true | 1 | '2'>(lastOf(trailingSpreadTuple3));
// Explicit `undefined` in optional elements
expectType<1 | 2 | undefined>({} as LastArrayElement<[0, 1, (2 | undefined)?]>);
expectType<1 | 2 | undefined | string>({} as LastArrayElement<readonly [0, 1, (2 | undefined)?, ...string[]]>);

// Required, optional, and rest element
expectType<2 | 3 | string>({} as LastArrayElement<[1, 2, 3?, ...string[]]>);
expectType<number | string | bigint>({} as LastArrayElement<readonly [number, string?, ...bigint[]]>);

// Optional and rest element
expectType<1 | string | undefined>({} as LastArrayElement<readonly [1?, ...string[]]>);
expectType<1 | 2 | 3 | bigint | undefined>({} as LastArrayElement<[1?, 2?, 3?, ...bigint[]]>);

// Labelled tuples
expectType<number>({} as LastArrayElement<[x: number, y: number]>);
expectType<string | number>({} as LastArrayElement<readonly [x: string, y?: number]>);
expectType<string | number>({} as LastArrayElement<[x: string, ...rest: number[]]>);

// Union elements
expectType<'d' | 'e'>({} as LastArrayElement<['a' | 'b', 'c', 'd' | 'e']>);
expectType<3 | 'c' | 'd' | 'e'>({} as LastArrayElement<[1 | 'a', 2 | 'b', 3 | 'c', ('d' | 'e')?]>);
expectType<boolean | string | number>({} as LastArrayElement<[bigint, boolean, ...Array<string | number>]>);

// Unions
expectType<3 | 6>({} as LastArrayElement<[1, 2, 3] | [4, 5, ...string[], 6]>);
expectType<number | bigint | undefined>({} as LastArrayElement<readonly [string, number] | bigint[]>);
expectType<1 | 2 | 3 | 'a' | 'b' | undefined | 'd' | 'e' | 'g'>(
{} as LastArrayElement<readonly [1, 2?, 3?] | ['a'?, 'b'?] | ['c', 'd', ...Array<'e'>] | [...Array<'f'>, 'g']>,
);

// Non-tuple arrays
expectType<string | undefined>({} as LastArrayElement<string[]>);
expectType<number | undefined>({} as LastArrayElement<readonly number[]>);
expectType<number | bigint | undefined>({} as LastArrayElement<ReadonlyArray<number | bigint>>);
expectType<any | undefined>({} as LastArrayElement<any[]>);
expectType<undefined>({} as any as LastArrayElement<never[]>);

// Boundary cases
expectType<any>({} as LastArrayElement<any>);
expectType<never>({} as LastArrayElement<never>);