LastArrayElement: Fix handling of tuples with optional elements#1461
LastArrayElement: Fix handling of tuples with optional elements#1461chatman-media wants to merge 7 commits into
LastArrayElement: Fix handling of tuples with optional elements#1461Conversation
|
|
||
| // The `| undefined` from a genuine `undefined`-containing array is preserved | ||
| declare const undefinedArray: Array<number | undefined>; | ||
| expectType<number | undefined>(lastOf(undefinedArray)); |
There was a problem hiding this comment.
| expectType<number | undefined>(lastOf(undefinedArray)); | |
| expectType<number | undefined>(lastOf(undefinedArray)); | |
| declare const optionalExplicitUndefinedTuple: [string, (number | undefined)?]; | |
| expectType<string | number | undefined>(lastOf(optionalExplicitUndefinedTuple)); | |
| declare const optionalExplicitUndefinedOnlyTuple: [(number | undefined)?]; | |
| expectType<number | undefined>(lastOf(optionalExplicitUndefinedOnlyTuple)); | |
| declare const optionalBeforeRestTuple: [string, number?, ...boolean[]]; | |
| expectType<string | number | boolean>(lastOf(optionalBeforeRestTuple)); |
|
I think the fix needs to distinguish For example, So |
|
Also, the presence of // @exactOptionalPropertyTypes: false
import type {LastArrayElement} from 'type-fest';
type T = LastArrayElement<[1, 2, 3?]>;
//=> 2 | 3 | undefined// @exactOptionalPropertyTypes: true
import type {LastArrayElement} from 'type-fest';
type T = LastArrayElement<[1, 2, 3?]>;
//=> 2 | 3Also, I think the result for empty tuples, non-tuple arrays (e.g., |
|
Here's an implementation that addresses all the concerns mentioned above. It's built using our 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';
export type LastArrayElement<TArray extends UnknownArray> =
IsAny<TArray> extends true
? any
: 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;And, here's the updated test suite: import {expectType} from 'tsd';
import type {LastArrayElement} from '../index.d.ts';
// Empty array
expectType<undefined>(undefined as LastArrayElement<[]>);
expectType<undefined>(undefined as LastArrayElement<readonly []>);
// Only required elements
expectType<boolean>({} as LastArrayElement<[number, string, boolean]>);
expectType<boolean>({} as LastArrayElement<readonly [number, string, boolean]>);
// 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?]>,
);
// 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
// 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?]>);
// 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>); |
|
Thanks @som-sm — applied your
I kept the existing JSDoc block and used your full test suite as-is. |
|
Tests should pass once #1462 is merged. |
Revert the `IfNotAnyOrNever` wrapper back to the inline `IsAny<TArray> 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 sindresorhus#1462 changes `IfNotAnyOrNever` to lazily evaluate its branches). The inline short-circuit keeps the body in a non-instantiated branch, so `LastArrayElement<any>` resolves to `any` and the whole suite passes on `main` today without depending on sindresorhus#1462.
|
@som-sm I kept your The only thing blocking the suite was the So I reverted that wrapper back to your inline That said, if you'd rather keep this consistent with |
|
Wrapping it in |
|
Updated overview of the PR:
|
|
Thanks @som-sm — I see you've already switched it back to I re-ran the full suite on the current branch head and it's green: Agreed on the updated overview: treating |
Problem
LastArrayElementreturns incorrect results for tuples that contain optional elements.A tuple with a trailing optional element (e.g.
[string, number?]) does not match either[...infer U, infer V]or[infer U, ...infer V], so it falls through to theReadonlyArray<infer U>branch. Because the element type of an optional member isT | undefined, a spurious| undefinedleaks into the result.Fix
In the
ReadonlyArray<infer U>branch, distinguish a genuine non-tuple array / trailing spread element (number extends Elements['length']) from a tuple made up solely of optional elements (finitelength). For the latter, the| undefinedintroduced by the optional modifier is removed.A genuine
undefined-containing array is left untouched, so existing behavior is preserved:Tests
Added cases to
test-d/last-array-element.tscovering trailing/leading optional elements, the readonly variant, and a regression guard ensuring genuineundefinedelement types are preserved. All new tests fail onmainand pass with the fix; the rest of the suite (tsd,tsc,xo) stays green.