From 4ef3929b60e2f422723964ad2908fffde11961a7 Mon Sep 17 00:00:00 2001 From: Alexander Kireev Date: Thu, 18 Jun 2026 01:25:51 +0700 Subject: [PATCH 1/6] `LastArrayElement`: Fix handling of tuples with optional elements --- source/last-array-element.d.ts | 6 +++++- test-d/last-array-element.ts | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/source/last-array-element.d.ts b/source/last-array-element.d.ts index a27e5eaa0..178516ec4 100644 --- a/source/last-array-element.d.ts +++ b/source/last-array-element.d.ts @@ -30,7 +30,11 @@ export type LastArrayElement : Elements extends ReadonlyArray - ? U | ElementBeforeTailingSpreadElement + ? number extends Elements['length'] + // A genuine non-tuple array (e.g., `number[]`) or trailing spread element — the last element can be the rest type or the element before it. + ? U | ElementBeforeTailingSpreadElement + // A tuple consisting solely of optional elements (e.g., `[string?, number?]`). The last element is the type of any of those elements (without the `| undefined` added by the optional modifier) or, when they are all absent, the element before them. + : Exclude | ElementBeforeTailingSpreadElement : never; export {}; diff --git a/test-d/last-array-element.ts b/test-d/last-array-element.ts index 6cc9980e6..757e878c4 100644 --- a/test-d/last-array-element.ts +++ b/test-d/last-array-element.ts @@ -23,3 +23,26 @@ expectType(lastOf(trailingSpreadTuple2)); // eslint-disable-next-line @typescript-eslint/array-type declare const trailingSpreadTuple3: ['foo', true, ...(1 | '2')[]]; expectType(lastOf(trailingSpreadTuple3)); + +// Tuples with optional elements +declare const trailingOptionalTuple1: [string, number?]; +expectType(lastOf(trailingOptionalTuple1)); + +declare const trailingOptionalTuple2: [number?]; +expectType(lastOf(trailingOptionalTuple2)); + +declare const trailingOptionalTuple3: [string, boolean, number?]; +expectType(lastOf(trailingOptionalTuple3)); + +declare const trailingOptionalTuple4: [string, number?, boolean?]; +expectType(lastOf(trailingOptionalTuple4)); + +declare const leadingOptionalTuple: [string?, number?]; +expectType(lastOf(leadingOptionalTuple)); + +declare const readonlyOptionalTuple: readonly [string, number?]; +expectType(lastOf(readonlyOptionalTuple)); + +// The `| undefined` from a genuine `undefined`-containing array is preserved +declare const undefinedArray: Array; +expectType(lastOf(undefinedArray)); From 31122d4f295d3674256f90fbe7314a31640f4552 Mon Sep 17 00:00:00 2001 From: Alexander Kireev Date: Fri, 19 Jun 2026 20:30:18 +0700 Subject: [PATCH 2/6] Handle optional and explicit-undefined elements in LastArrayElement --- source/last-array-element.d.ts | 49 +++++++++++++------- test-d/last-array-element.ts | 85 +++++++++++++++++++++------------- 2 files changed, 84 insertions(+), 50 deletions(-) diff --git a/source/last-array-element.d.ts b/source/last-array-element.d.ts index 178516ec4..a04d6d2db 100644 --- a/source/last-array-element.d.ts +++ b/source/last-array-element.d.ts @@ -1,3 +1,9 @@ +import type {If} from './if.d.ts'; +import type {IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts'; +import type {IsAny} from './is-any.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. @@ -19,22 +25,31 @@ const last2 = lastOf([true, false, 'baz', 10]); @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 - ? number extends Elements['length'] - // A genuine non-tuple array (e.g., `number[]`) or trailing spread element — the last element can be the rest type or the element before it. - ? U | ElementBeforeTailingSpreadElement - // A tuple consisting solely of optional elements (e.g., `[string?, number?]`). The last element is the type of any of those elements (without the `| undefined` added by the optional modifier) or, when they are all absent, the element before them. - : Exclude | ElementBeforeTailingSpreadElement - : never; +export type LastArrayElement = + IsAny extends true + ? any + : TArray extends UnknownArray // For distributing `TArray` + ? SplitOnRestElement 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 757e878c4..c1a25be7d 100644 --- a/test-d/last-array-element.ts +++ b/test-d/last-array-element.ts @@ -1,48 +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); -// Tuples with optional elements -declare const trailingOptionalTuple1: [string, number?]; -expectType(lastOf(trailingOptionalTuple1)); +// Required, optional, and rest element +expectType<2 | 3 | string>({} as LastArrayElement<[1, 2, 3?, ...string[]]>); +expectType({} as LastArrayElement); -declare const trailingOptionalTuple2: [number?]; -expectType(lastOf(trailingOptionalTuple2)); +// Optional and rest element +expectType<1 | string | undefined>({} as LastArrayElement); +expectType<1 | 2 | 3 | bigint | undefined>({} as LastArrayElement<[1?, 2?, 3?, ...bigint[]]>); -declare const trailingOptionalTuple3: [string, boolean, number?]; -expectType(lastOf(trailingOptionalTuple3)); +// Labelled tuples +expectType({} as LastArrayElement<[x: number, y: number]>); +expectType({} as LastArrayElement); +expectType({} as LastArrayElement<[x: string, ...rest: number[]]>); -declare const trailingOptionalTuple4: [string, number?, boolean?]; -expectType(lastOf(trailingOptionalTuple4)); +// 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]>); -declare const leadingOptionalTuple: [string?, number?]; -expectType(lastOf(leadingOptionalTuple)); +// 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']>, +); -declare const readonlyOptionalTuple: readonly [string, number?]; -expectType(lastOf(readonlyOptionalTuple)); +// Non-tuple arrays +expectType({} as LastArrayElement); +expectType({} as LastArrayElement); +expectType({} as LastArrayElement>); +expectType({} as LastArrayElement); +expectType({} as any as LastArrayElement); -// The `| undefined` from a genuine `undefined`-containing array is preserved -declare const undefinedArray: Array; -expectType(lastOf(undefinedArray)); +// Boundary cases +expectType({} as LastArrayElement); +expectType({} as LastArrayElement); From ebd49d485c52815a75f04d464563f603acdcd50a Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee <49264891+som-sm@users.noreply.github.com> Date: Sat, 20 Jun 2026 20:29:13 +0530 Subject: [PATCH 3/6] refactor: use `IfNotAnyOrNever` --- source/last-array-element.d.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/source/last-array-element.d.ts b/source/last-array-element.d.ts index a04d6d2db..abe570537 100644 --- a/source/last-array-element.d.ts +++ b/source/last-array-element.d.ts @@ -1,6 +1,5 @@ import type {If} from './if.d.ts'; -import type {IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts'; -import type {IsAny} from './is-any.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'; @@ -26,13 +25,12 @@ const last2 = lastOf([true, false, 'baz', 10]); @category Template literal */ export type LastArrayElement = - IsAny extends true - ? any - : TArray extends UnknownArray // For distributing `TArray` + IfNotAnyOrNever extends readonly [infer BeforeRest extends UnknownArray, infer Rest extends UnknownArray, infer AfterRest extends UnknownArray] ? _LastArrayElement : never - : never; + : never>; type _LastArrayElement = AfterRest extends readonly [...any, infer Last] // Note there are no optional elements in `AfterRest`. From d1ec1ffbc1c005d32e264c4b1999242a9981f60d Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee <49264891+som-sm@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:24:38 +0530 Subject: [PATCH 4/6] doc: add notes about optional/rest/empty cases --- source/last-array-element.d.ts | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/source/last-array-element.d.ts b/source/last-array-element.d.ts index abe570537..eaa271b94 100644 --- a/source/last-array-element.d.ts +++ b/source/last-array-element.d.ts @@ -21,6 +21,41 @@ 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 */ From 765028373ccb50f5033963db0a907efb05e2550f Mon Sep 17 00:00:00 2001 From: Alexander Kireyev Date: Mon, 22 Jun 2026 13:58:33 +0700 Subject: [PATCH 5/6] `LastArrayElement`: Short-circuit `any` with an inline `IsAny` guard Revert the `IfNotAnyOrNever` wrapper back to the inline `IsAny extends true ? any : ...` form from the proposed implementation. The wrapper eagerly instantiates the recursive `SplitOnRestElement` body for `any`, producing "Type instantiation is excessively deep" (only resolved once #1462 changes `IfNotAnyOrNever` to lazily evaluate its branches). The inline short-circuit keeps the body in a non-instantiated branch, so `LastArrayElement` resolves to `any` and the whole suite passes on `main` today without depending on #1462. --- source/last-array-element.d.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/source/last-array-element.d.ts b/source/last-array-element.d.ts index eaa271b94..2e9b9f8a3 100644 --- a/source/last-array-element.d.ts +++ b/source/last-array-element.d.ts @@ -1,5 +1,6 @@ import type {If} from './if.d.ts'; -import type {IfNotAnyOrNever, IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts'; +import type {IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts'; +import type {IsAny} from './is-any.d.ts'; import type {SplitOnRestElement} from './split-on-rest-element.d.ts'; import type {UnknownArray} from './unknown-array.d.ts'; @@ -60,12 +61,13 @@ type D = LastArrayElement<[string?, number?, ...bigint[]]>; @category Template literal */ export type LastArrayElement = - IfNotAnyOrNever extends true + ? any + : TArray extends UnknownArray // For distributing `TArray` ? SplitOnRestElement extends readonly [infer BeforeRest extends UnknownArray, infer Rest extends UnknownArray, infer AfterRest extends UnknownArray] ? _LastArrayElement : never - : never>; + : never; type _LastArrayElement = AfterRest extends readonly [...any, infer Last] // Note there are no optional elements in `AfterRest`. From 4b927c09b442ecbab7b5c8227d27f3110ecebecb Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee <49264891+som-sm@users.noreply.github.com> Date: Tue, 23 Jun 2026 05:18:37 +0530 Subject: [PATCH 6/6] refactor: switch to using `IfNotAnyOrNever` --- source/last-array-element.d.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/source/last-array-element.d.ts b/source/last-array-element.d.ts index 2e9b9f8a3..7c67ffdc2 100644 --- a/source/last-array-element.d.ts +++ b/source/last-array-element.d.ts @@ -1,6 +1,5 @@ import type {If} from './if.d.ts'; -import type {IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts'; -import type {IsAny} from './is-any.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'; @@ -61,13 +60,13 @@ type D = LastArrayElement<[string?, number?, ...bigint[]]>; @category Template literal */ export type LastArrayElement = - IsAny extends true - ? any - : TArray extends UnknownArray // For distributing `TArray` + 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`.