Skip to content
Draft
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 @@ -163,6 +163,7 @@ export type {IsUppercase} from './source/is-uppercase.d.ts';
export type {IsOptional} from './source/is-optional.d.ts';
export type {IsNullable} from './source/is-nullable.d.ts';
export type {TupleOf} from './source/tuple-of.d.ts';
export type {ArrayAt} from './source/array-at.d.ts';

// Template literal types
export type {CamelCase, CamelCaseOptions} from './source/camel-case.d.ts';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ Click the type names for complete docs.
- [`SplitOnRestElement`](source/split-on-rest-element.d.ts) - Splits an array into three parts, where the first contains all elements before the rest element, the second is the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element itself, and the third contains all elements after the rest element.
- [`ExtractRestElement`](source/extract-rest-element.d.ts) - Extract the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element type from an array.
- [`ExcludeRestElement`](source/exclude-rest-element.d.ts) - Create a tuple with the [`rest`](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) element removed.
- [`ArrayAt`](source/array-at.d.ts) - Return the element at the given index of the given array.

### Numeric

Expand Down
146 changes: 146 additions & 0 deletions source/array-at.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import type {ExcludeRestElement} from './exclude-rest-element.d.ts';
import type {ExtractRestElement} from './extract-rest-element.d.ts';
import type {If} from './if.d.ts';
import type {IntRange} from './int-range.d.ts';
import type {IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts';
import type {IsOptionalKeyOf} from './is-optional-key-of.d.ts';
import type {LessThan} from './less-than.d.ts';
import type {IsNegative} from './numeric.d.ts';
import type {SplitOnRestElement} from './split-on-rest-element.d.ts';
import type {Subtract} from './subtract.d.ts';
import type {Sum} from './sum.d.ts';
import type {TupleOf} from './tuple-of.d.ts';
import type {UnknownArray} from './unknown-array.d.ts';

/**
Return the element at the given index of the given array.

Use-case: Get the element at a specific index of an array.

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

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

// Positive index
type A = ArrayAt<['a', 'b', 'c', 'd'], 1>;
//=> 'b'

// Negative index
type B = ArrayAt<['a', 'b', 'c', 'd'], -4>;
//=> 'a'

// Positive index with optional elements
type C = ArrayAt<['a', 'b', 'c'?, 'd'?], 3>;
//=> 'd' | undefined

// Negative index with optional elements
type D = ArrayAt<['a', 'b', 'c'?, 'd'?], -2>;
//=> 'a' | 'b' | 'c'

// Positive index with rest element and optional elements
type E = ArrayAt<['a', 'b', 'c'?, ...number[]], 3>;
//=> number | undefined

// Negative index with rest element and optional elements
type F = ArrayAt<['a', 'b', 'c'?, ...number[]], -1>;
//=> 'b' | 'c' | number

// Positive index with rest element in middle
type G = ArrayAt<['a', 'b', 'c', ...number[], 'd', 'e'], 4>;
//=> number | 'd' | 'e'

// Negative index with rest element in middle
type H = ArrayAt<['a', 'b', ...number[], 'c', 'd', 'e'], -5>;
//=> 'a' | 'b' | number

// Out-of-bounds positive index
type I = ArrayAt<['a', 'b'], 5>;
//=> undefined

// Out-of-bounds negative index
type J = ArrayAt<['a', 'b'], -3>;
//=> undefined
*/
export type ArrayAt<TArray extends UnknownArray, Index extends number> =
TArray extends unknown // For distributing `Array_`
? Index extends unknown // For distributing `Index`
? number extends Index
? TArray[number] | undefined
: IsNegative<Index> extends true
? ArrayAtNegativeIndex<TArray, Index>
: ArrayAtPositiveIndex<TArray, Index>
: never // Should never happen
: never; // Should never happen

type ArrayAtPositiveIndex<TArray extends UnknownArray, Index extends number> =
SplitOnRestElement<TArray> extends readonly [infer BeforeRest extends UnknownArray, infer Rest extends UnknownArray, infer AfterRest extends UnknownArray]
? LessThan<Index, Required<BeforeRest>['length']> extends true
? BeforeRest[Index]
: Rest[number] | AfterRest[IntRange<0, Sum<Subtract<Index, Required<BeforeRest>['length']>, 1>>]
: never; // Should never happen

/**
Recursion order for `ArrayAtNegativeIndex<['a', 'b', 'c', ...number[], 'd', 'e'], -5>`:

1. `ArrayAtNegativeIndex<['a', 'b', 'c', ...number[], 'd', 'e'], -5, 0, never>` // No match, increment `Index`.
2. `ArrayAtNegativeIndex<['a', 'b', 'c', ...number[], 'd'], -4, 0, never>` // No match, increment `Index`.
3. `ArrayAtNegativeIndex<['a', 'b', 'c', ...number[]], -3, 0, never>` // Found rest element, set `Index` to `-1`, `Left` to `Sum<Index, 1>` (i.e., `-2`), add rest element to result.
4. `ArrayAtNegativeIndex<['a', 'b', 'c'], -1, -2, number>` // Match found, `Left` not yet `0`, increment `Left`, add current element to result.
5. `ArrayAtNegativeIndex<['a', 'b'], -1, -1, 'c' | number>` // Match found, `Left` not yet `0`, increment `Left`, add current element to result.
6. `ArrayAtNegativeIndex<['a'], -1, 0, 'b' | 'c' | number>` // Match found, `Left` is `0`, add current element to result and return result.

Result: `'a' | 'b' | 'c' | number`

---

Recursion order for `ArrayAtNegativeIndex<['a', 'b', 'c'?, ...number[]], -1>`:

1. `ArrayAtNegativeIndex<['a', 'b', 'c'?, ...number[]], -1, 0, never>` // Found rest element, set `Index` to `-1`, `Left` to `Sum<Index, 1>` (i.e., `0`), add rest element to result.
2. `ArrayAtNegativeIndex<['a', 'b', 'c'?], -1, 0, number>` // Match found, current element is optional, add it to result without changing anything else.
3. `ArrayAtNegativeIndex<['a', 'b'], -1, 0, number | 'c'>` // Match found, current element is not optional, add it to result and return result.

Result: `'b' | 'c' | number`

---

Recursion order for `ArrayAt<['a', 'b', 'c', 'd'?, 'e'?], -3>`:

1. `ArrayAtNegativeIndex<['a', 'b', 'c', 'd'?, 'e'?], -3, 0, never>` // No match, current element is optional, increment `Index` & decrement `Left`.
2. `ArrayAtNegativeIndex<['a', 'b', 'c', 'd'?], -2, -1, never>` // No match, current element is optional, increment `Index` & decrement `Left`.
3. `ArrayAtNegativeIndex<['a', 'b', 'c'], -1, -2, never>` // Match found, current element is not optional, `Left` is not `0`, increment `Left`, add current element to result.
4. `ArrayAtNegativeIndex<['a', 'b'], -1, -1, 'c'>` // Match found, current element is not optional, `Left` is not `0`, increment `Left`, add current element to result.
5. `ArrayAtNegativeIndex<['a'], -1, 0, 'b' | 'c'>` // Match found, current element is not optional, `Left` is `0`, add current element to result and return result.

Result: `'a' | 'b' | 'c'`
*/
type ArrayAtNegativeIndex<TArray extends UnknownArray, Index extends number, Left extends number = 0, Result = never> =
TArray extends readonly []
? Result | undefined // If the array is exhausted, return `Result` with `undefined`.
: number extends TArray['length']
// Enters this branch, if `TArray` contains a rest element.
? TArray extends readonly [...infer Rest, infer L]
? Index extends -1
? L // If an element after the rest element matches, return it.
: ArrayAtNegativeIndex<Rest, Sum<Index, 1>, Left, Result>
// Enters this branch, if `TArray` contains no elements after the rest element.
: ExcludeRestElement<TArray> extends infer TWithoutRest extends UnknownArray
// Remove the rest element and recurse further with the elements before the rest element,
// Set `Index` to `-1` & `Left` to `Sum<Index, 1>`, so that elements keep getting matched until `Left` reaches `0`.
? ArrayAtNegativeIndex<TWithoutRest, -1, Sum<Index, 1>, ExtractRestElement<TArray> | Result> // Also, add the rest element to `Result`.
: never // Should never happen
// Enters this branch, if `TArray` contains no rest element.
: TArray extends readonly [...infer Rest, (infer Last)?]
? Index extends -1
? TArray extends readonly [...infer Rest, infer Last] // If `Last` is not optional
? Left extends 0
? Last | Result // If there's a match, and `Left` is `0`, return `Result` with `Last`.
: ArrayAtNegativeIndex<Rest, Index, Sum<Left, 1>, Last | Result> // If there's a match, and `Left` is not `0`, increment `Left` and add `Last` to `Result`.
: ArrayAtNegativeIndex<Rest, Index, Left, Last | If<IsExactOptionalPropertyTypesEnabled, never, undefined> | Result> // If `Last` is optional, just add it to `Result` without changing anything else.
: TArray extends readonly [...infer Rest, unknown]
? ArrayAtNegativeIndex<Rest, Sum<Index, 1>, Left, Result> // If `Last` is not optional, just increment `Index`.
: ArrayAtNegativeIndex<Rest, Sum<Index, 1>, Subtract<Left, 1>, Result> // If `Last` is optional, increment `Index` and decrement `Left`.
: never; // Should never happen

export {};
103 changes: 103 additions & 0 deletions test-d/array-at.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {expectType} from 'tsd';
import type {ArrayAt} from '../index.d.ts';

expectType<ArrayAt<[string], 0>>({} as string);
expectType<ArrayAt<[string], -1>>({} as string);

expectType<ArrayAt<[], 0>>(undefined);
expectType<ArrayAt<[], -1>>(undefined);

expectType<ArrayAt<[number, boolean], 0>>({} as number);
expectType<ArrayAt<[number, boolean], 1>>({} as boolean);
expectType<ArrayAt<[number, boolean], 2>>(undefined);
expectType<ArrayAt<[number, boolean], 99>>(undefined);
expectType<ArrayAt<[number, boolean], -1>>({} as boolean);
expectType<ArrayAt<[number, boolean], -2>>({} as number);
expectType<ArrayAt<[number, boolean], -3>>(undefined);
expectType<ArrayAt<[number, boolean], -99>>(undefined);

expectType<ArrayAt<[number, boolean?], 0>>({} as number);
expectType<ArrayAt<[number, boolean?], 1>>({} as boolean | undefined);
expectType<ArrayAt<[number, boolean?], 2>>(undefined);
expectType<ArrayAt<[number, boolean?], 99>>(undefined);
expectType<ArrayAt<[number, boolean?], -1>>({} as number | boolean);
expectType<ArrayAt<[number, boolean?], -2>>({} as number | undefined);
expectType<ArrayAt<[number, boolean?], -3>>(undefined);
expectType<ArrayAt<[number, boolean?], -99>>(undefined);

type StaticArray = [object, number, boolean?, string?];
expectType<ArrayAt<StaticArray, 0>>({} as object);
expectType<ArrayAt<StaticArray, 1>>({} as number);
expectType<ArrayAt<StaticArray, 2>>({} as boolean | undefined);
expectType<ArrayAt<StaticArray, 3>>({} as string | undefined);
expectType<ArrayAt<StaticArray, -1>>({} as string | boolean | number);
expectType<ArrayAt<StaticArray, -2>>({} as boolean | number | object);
expectType<ArrayAt<StaticArray, -3>>({} as number | object | undefined);
expectType<ArrayAt<StaticArray, -4>>({} as object | undefined);
expectType<ArrayAt<StaticArray, -5>>(undefined);
expectType<ArrayAt<StaticArray, -99>>(undefined);

type LeadingSpreadArray = [...string[], number, boolean];
expectType<ArrayAt<LeadingSpreadArray, 0>>({} as string | number);
expectType<ArrayAt<LeadingSpreadArray, 1>>({} as string | number | boolean);
expectType<ArrayAt<LeadingSpreadArray, 2>>({} as string | number | boolean | undefined);
expectType<ArrayAt<LeadingSpreadArray, 3>>({} as string | number | boolean | undefined);
expectType<ArrayAt<LeadingSpreadArray, -1>>({} as boolean);
expectType<ArrayAt<LeadingSpreadArray, -2>>({} as number);
expectType<ArrayAt<LeadingSpreadArray, -3>>({} as string | undefined);

type TrailingSpreadArray = [number, boolean, ...string[]];
expectType<ArrayAt<TrailingSpreadArray, 0>>({} as number);
expectType<ArrayAt<TrailingSpreadArray, 1>>({} as boolean);
expectType<ArrayAt<TrailingSpreadArray, 2>>({} as string | undefined);
expectType<ArrayAt<TrailingSpreadArray, 3>>({} as string | undefined);
expectType<ArrayAt<TrailingSpreadArray, -1>>({} as string | boolean);
expectType<ArrayAt<TrailingSpreadArray, -2>>({} as boolean | string | number);
expectType<ArrayAt<TrailingSpreadArray, -3>>({} as boolean | string | number | undefined);

type TrailingSpreadArray2 = [object, number, boolean?, ...string[]];
expectType<ArrayAt<TrailingSpreadArray2, 0>>({} as object);
expectType<ArrayAt<TrailingSpreadArray2, 1>>({} as number);
expectType<ArrayAt<TrailingSpreadArray2, 2>>({} as boolean | undefined);
expectType<ArrayAt<TrailingSpreadArray2, 3>>({} as string | undefined);
expectType<ArrayAt<TrailingSpreadArray2, -1>>({} as string | number | boolean);
expectType<ArrayAt<TrailingSpreadArray2, -2>>({} as string | number | boolean | object);
expectType<ArrayAt<TrailingSpreadArray2, -3>>({} as string | number | boolean | object | undefined);

type MiddleSpreadArray = [number, ...string[], boolean];
expectType<ArrayAt<MiddleSpreadArray, 0>>({} as number);
expectType<ArrayAt<MiddleSpreadArray, 1>>({} as string | boolean);
expectType<ArrayAt<MiddleSpreadArray, 2>>({} as string | boolean | undefined);
expectType<ArrayAt<MiddleSpreadArray, 3>>({} as string | boolean | undefined);
expectType<ArrayAt<MiddleSpreadArray, -1>>({} as boolean);
expectType<ArrayAt<MiddleSpreadArray, -2>>({} as string | number);
expectType<ArrayAt<MiddleSpreadArray, -3>>({} as string | number | undefined);

type UnionArray = [string, number] | [boolean, string?];
expectType<ArrayAt<UnionArray, 0>>({} as string | boolean);
expectType<ArrayAt<UnionArray, 1>>({} as number | string | undefined);
expectType<ArrayAt<UnionArray, -1>>({} as number | string | boolean);
expectType<ArrayAt<UnionArray, -2>>({} as undefined | string | boolean);

expectType<ArrayAt<string[], 0>>({} as string | undefined);
expectType<ArrayAt<string[], 1>>({} as string | undefined);
expectType<ArrayAt<string[], 2>>({} as string | undefined);
expectType<ArrayAt<string[], -1>>({} as string | undefined);
expectType<ArrayAt<string[], -2>>({} as string | undefined);
expectType<ArrayAt<string[], -3>>({} as string | undefined);

expectType<ArrayAt<readonly string[], 0>>({} as string | undefined);
expectType<ArrayAt<readonly string[], 1>>({} as string | undefined);
expectType<ArrayAt<readonly string[], 2>>({} as string | undefined);
expectType<ArrayAt<readonly string[], -1>>({} as string | undefined);
expectType<ArrayAt<readonly string[], -2>>({} as string | undefined);
expectType<ArrayAt<readonly string[], -3>>({} as string | undefined);

expectType<ArrayAt<readonly [string, number], 0>>({} as string);
expectType<ArrayAt<readonly [string, number], 1>>({} as number);
expectType<ArrayAt<readonly [string, number], 2>>(undefined);
expectType<ArrayAt<readonly [string, number], -1>>({} as number);

// Test unknown index
expectType<ArrayAt<[1, 2, 3], number>>({} as 1 | 2 | 3 | undefined);
expectType<ArrayAt<string[], number>>({} as string | undefined);