diff --git a/source/last-array-element.d.ts b/source/last-array-element.d.ts index a27e5eaa0..7c67ffdc2 100644 --- a/source/last-array-element.d.ts +++ b/source/last-array-element.d.ts @@ -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. @@ -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 | 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 = - // 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 - : Elements extends ReadonlyArray - ? U | ElementBeforeTailingSpreadElement - : never; +export type LastArrayElement = + IfNotAnyOrNever extends readonly [infer BeforeRest extends UnknownArray, infer Rest extends UnknownArray, infer AfterRest extends UnknownArray] + ? _LastArrayElement + : never + : never; + }>; + +type _LastArrayElement = + 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; // Otherwise, the result is union of the `Rest` element and the last element in `BeforeRest`. + +type BeforeRestLastElement = + 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 + > + : never; export {}; diff --git a/test-d/last-array-element.ts b/test-d/last-array-element.ts index 6cc9980e6..c1a25be7d 100644 --- a/test-d/last-array-element.ts +++ b/test-d/last-array-element.ts @@ -1,25 +1,67 @@ import {expectType} from 'tsd'; import type {LastArrayElement} from '../index.d.ts'; -declare function lastOf(array: V): LastArrayElement; -const array: ['foo', 2, 'bar'] = ['foo', 2, 'bar']; -const mixedArray: ['bar', 'foo', 2] = ['bar', 'foo', 2]; +// Empty array +expectType(undefined as LastArrayElement<[]>); +expectType(undefined as LastArrayElement); -expectType<'bar'>(lastOf(array)); -expectType<2>(lastOf(mixedArray)); -expectType(lastOf(['a', 'b', 'c'])); -expectType(lastOf(['a', 'b', 1])); -expectType<1>(lastOf(['a', 'b', 1] as const)); +// Only required elements +expectType({} as LastArrayElement<[number, string, boolean]>); +expectType({} as LastArrayElement); -declare const leadingSpreadTuple: [...string[], object, number]; -expectType(lastOf(leadingSpreadTuple)); +// Required and optional elements +expectType<2 | 3>({} as LastArrayElement<[1, 2, 3?]>); +expectType<1 | 2 | 3>({} as LastArrayElement); +expectType( + {} as LastArrayElement<[null, string, bigint?, boolean?, number?]>, +); -declare const trailingSpreadTuple1: [string, ...number[]]; -expectType(lastOf(trailingSpreadTuple1)); +// Required and rest element +expectType<3 | string>({} as LastArrayElement); // Rest element at the end +expectType<'z'>({} as LastArrayElement<[1, 2, 3, ...boolean[], 'x', 'y', 'z']>); // Rest element in the middle +expectType({} as LastArrayElement); // Rest element at the start -declare const trailingSpreadTuple2: [string, boolean, ...number[]]; -expectType(lastOf(trailingSpreadTuple2)); +// Only optional elements +expectType<'a' | undefined>({} as LastArrayElement<['a'?]>); +expectType<{data: string[]} | undefined>({} as LastArrayElement); +expectType({} as LastArrayElement<[string?, boolean?, bigint?]>); -// eslint-disable-next-line @typescript-eslint/array-type -declare const trailingSpreadTuple3: ['foo', true, ...(1 | '2')[]]; -expectType(lastOf(trailingSpreadTuple3)); +// Explicit `undefined` in optional elements +expectType<1 | 2 | undefined>({} as LastArrayElement<[0, 1, (2 | undefined)?]>); +expectType<1 | 2 | undefined | string>({} as LastArrayElement); + +// Required, optional, and rest element +expectType<2 | 3 | string>({} as LastArrayElement<[1, 2, 3?, ...string[]]>); +expectType({} as LastArrayElement); + +// Optional and rest element +expectType<1 | string | undefined>({} as LastArrayElement); +expectType<1 | 2 | 3 | bigint | undefined>({} as LastArrayElement<[1?, 2?, 3?, ...bigint[]]>); + +// Labelled tuples +expectType({} as LastArrayElement<[x: number, y: number]>); +expectType({} as LastArrayElement); +expectType({} 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({} as LastArrayElement<[bigint, boolean, ...Array]>); + +// Unions +expectType<3 | 6>({} as LastArrayElement<[1, 2, 3] | [4, 5, ...string[], 6]>); +expectType({} as LastArrayElement); +expectType<1 | 2 | 3 | 'a' | 'b' | undefined | 'd' | 'e' | 'g'>( + {} as LastArrayElement] | [...Array<'f'>, 'g']>, +); + +// Non-tuple arrays +expectType({} as LastArrayElement); +expectType({} as LastArrayElement); +expectType({} as LastArrayElement>); +expectType({} as LastArrayElement); +expectType({} as any as LastArrayElement); + +// Boundary cases +expectType({} as LastArrayElement); +expectType({} as LastArrayElement);