From 7d47b8d49649c0995318b81596dc05732293fe22 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sun, 19 Oct 2025 14:13:08 +0530 Subject: [PATCH 1/9] feat: add `ArrayAt` type --- index.d.ts | 1 + source/array-at.d.ts | 91 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 source/array-at.d.ts diff --git a/index.d.ts b/index.d.ts index 7caefcf06..1bdbb6fb2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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'; diff --git a/source/array-at.d.ts b/source/array-at.d.ts new file mode 100644 index 000000000..9c94e5f59 --- /dev/null +++ b/source/array-at.d.ts @@ -0,0 +1,91 @@ +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 {IsOptionalKeyOf} from './is-optional-key-of.d.ts'; +import type {IsNegative} from './numeric.d.ts'; +import type {Subtract} from './subtract.d.ts'; +import type {Sum} from './sum.d.ts'; +import type {UnknownArray} from './unknown-array.d.ts'; + +export type ArrayAt = + TArray extends unknown // For distributing `Array_` + ? Index extends unknown // For distributing `Index` + ? number extends Index + ? TArray[number] | undefined + : IsNegative extends true + ? ArrayAtNegativeIndex + : ArrayAtPositiveIndex + : never // Should never happen + : never; // Should never happen + +/** +Recursion order for `ArrayAtPositiveIndex<["a", "b", ...number[], "c", "d", "e"], 4>`: + +1. `ArrayAtPositiveIndex<["a", "b", ...number[], "c", "d", "e"], 4>` // No match, decrement `Index`. +2. `ArrayAtPositiveIndex<["b", ...number[], "c", "d", "e"], 3, 0, never>` // No match, decrement `Index`. +3. `ArrayAtPositiveIndex<[...number[], "c", "d", "e"], 2, 0, number>` // Found rest element, set `Index` to `0`, `Right` to `Index` (i.e., `2`), add rest element to result. +4. `ArrayAtPositiveIndex<["c", "d", "e"], 0, 2, number | "c">` // Match found, `Right` not yet `0`, decrement `Right`, add current element to result. +5. `ArrayAtPositiveIndex<["d", "e"], 0, 1, number | "c" | "d">` // Match found, `Right` not yet `0`, decrement `Right`, add current element to result. +6. `ArrayAtPositiveIndex<["e"], 0, 0, number | "c" | "d" | "e">` // Match found, `Right` is `0`, add current element to result and return result. +7. Result: `number | "c" | "d" | "e"` +*/ +type ArrayAtPositiveIndex = + TArray extends readonly [] + ? Result | undefined // If the array is exhausted, and `Index` hasn't been found yet, return `Result` with `undefined`. + : keyof TArray & `${number}` extends never + // Enters this branch, if `TArray` is empty (e.g., `[]`), + // or `TArray` contains no non-rest elements preceding the rest element (e.g., `[...string[]]` or `[...string[], string]`). + ? ExcludeRestElement extends infer TWithoutRest extends UnknownArray + // Remove the rest element and recurse further with the elements AFTER the rest element, + // Set `Index` to `0` & `Right` to `Index`, so that elements keep getting matched until `Right` reaches `0`. + // Also, add the rest element to `Result`. + ? ArrayAtPositiveIndex> + : never // Should never happen + : TArray extends readonly [(infer First)?, ...infer Rest] + ? Index extends 0 + ? Right extends 0 + ? Result | First | If, undefined, never> // If there's a match, and `Right` is `0`, return `Result` with the current element. + : ArrayAtPositiveIndex, Result | First> // Enters this branch for elements after the rest element, here we add the current element to the result and decrement `Right`. + : ArrayAtPositiveIndex, Right, Result> // Enters this branch for elements before the rest element, here we decrement `Index` and recurse further. + : 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` (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. +7. Result: "a" | "b" | "c" | number +*/ +type ArrayAtNegativeIndex = + TArray extends readonly [] + ? Result | undefined // If the array is exhausted, and `Index` hasn't been found yet, 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, Left, Result> // Otherwise, decrement `Index`. + // Enters this branch, if `TArray` contains no elements after the rest element. + : ExcludeRestElement 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`, so that elements keep getting matched until `Left` reaches `0`. + // Also, add the rest element to `Result`. + ? ArrayAtNegativeIndex, ExtractRestElement | Result> + : never // Should never happen + : 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, Last | Result> // If there's a match, and `Left` is not `0`, increment `Left` and add `Last` to `Result`. + : ArrayAtNegativeIndex // If `Last` is optional, just add it to `Result` without changing anything else. + : TArray extends readonly [...infer Rest, unknown] + ? ArrayAtNegativeIndex, Left, Result> // If current element is not optional, just increment `Index`. + : ArrayAtNegativeIndex, Subtract, Result> // If current element is optional, increment `Index` and decrement `Left`. + : never; // Should never happen + +export {}; From 7b710673a45a2c44e1850d8874d30ed68e15466f Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sun, 19 Oct 2025 14:14:05 +0530 Subject: [PATCH 2/9] test: add cases for `ArrayAt` Co-authored-by: Wei Wen --- test-d/array-at.ts | 103 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 test-d/array-at.ts diff --git a/test-d/array-at.ts b/test-d/array-at.ts new file mode 100644 index 000000000..4dafe69ea --- /dev/null +++ b/test-d/array-at.ts @@ -0,0 +1,103 @@ +import {expectType} from 'tsd'; +import type {ArrayAt} from '../index.d.ts'; + +expectType>({} as string); +expectType>({} as string); + +expectType>(undefined); +expectType>(undefined); + +expectType>({} as number); +expectType>({} as boolean); +expectType>(undefined); +expectType>(undefined); +expectType>({} as boolean); +expectType>({} as number); +expectType>(undefined); +expectType>(undefined); + +expectType>({} as number); +expectType>({} as boolean | undefined); +expectType>(undefined); +expectType>(undefined); +expectType>({} as number | boolean); +expectType>({} as number | undefined); +expectType>(undefined); +expectType>(undefined); + +type StaticArray = [object, number, boolean?, string?]; +expectType>({} as object); +expectType>({} as number); +expectType>({} as boolean | undefined); +expectType>({} as string | undefined); +expectType>({} as string | boolean | number); +expectType>({} as boolean | number | object); +expectType>({} as number | object | undefined); +expectType>({} as object | undefined); +expectType>(undefined); +expectType>(undefined); + +type LeadingSpreadArray = [...string[], number, boolean]; +expectType>({} as string | number); +expectType>({} as string | number | boolean); +expectType>({} as string | number | boolean | undefined); +expectType>({} as string | number | boolean | undefined); +expectType>({} as boolean); +expectType>({} as number); +expectType>({} as string | undefined); + +type TrailingSpreadArray = [number, boolean, ...string[]]; +expectType>({} as number); +expectType>({} as boolean); +expectType>({} as string | undefined); +expectType>({} as string | undefined); +expectType>({} as string | boolean); +expectType>({} as boolean | string | number); +expectType>({} as boolean | string | number | undefined); + +type TrailingSpreadArray2 = [object, number, boolean?, ...string[]]; +expectType>({} as object); +expectType>({} as number); +expectType>({} as boolean | undefined); +expectType>({} as string | undefined); +expectType>({} as string | number | boolean); +expectType>({} as string | number | boolean | object); +expectType>({} as string | number | boolean | object | undefined); + +type MiddleSpreadArray = [number, ...string[], boolean]; +expectType>({} as number); +expectType>({} as string | boolean); +expectType>({} as string | boolean | undefined); +expectType>({} as string | boolean | undefined); +expectType>({} as boolean); +expectType>({} as string | number); +expectType>({} as string | number | undefined); + +type UnionArray = [string, number] | [boolean, string?]; +expectType>({} as string | boolean); +expectType>({} as number | string | undefined); +expectType>({} as number | string | boolean); +expectType>({} as undefined | string | boolean); + +expectType>({} as string | undefined); +expectType>({} as string | undefined); +expectType>({} as string | undefined); +expectType>({} as string | undefined); +expectType>({} as string | undefined); +expectType>({} as string | undefined); + +expectType>({} as string | undefined); +expectType>({} as string | undefined); +expectType>({} as string | undefined); +expectType>({} as string | undefined); +expectType>({} as string | undefined); +expectType>({} as string | undefined); + +expectType>({} as string); +expectType>({} as number); +expectType>(undefined); +expectType>({} as number); + +// Test unknown index +expectType>({} as 1 | 2 | 3 | undefined); +expectType>({} as string | undefined); From 66bf970c1f35442ffddb9e26611b5818f0734412 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sun, 19 Oct 2025 15:03:33 +0530 Subject: [PATCH 3/9] doc: add JSDoc --- source/array-at.d.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/source/array-at.d.ts b/source/array-at.d.ts index 9c94e5f59..0271ee19f 100644 --- a/source/array-at.d.ts +++ b/source/array-at.d.ts @@ -7,6 +7,57 @@ import type {Subtract} from './subtract.d.ts'; import type {Sum} from './sum.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 unknown // For distributing `Array_` ? Index extends unknown // For distributing `Index` From d845941bcd1b0ce5c8fd96909e00135683c18041 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sun, 19 Oct 2025 15:04:06 +0530 Subject: [PATCH 4/9] chore: update README --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index ca0d94f24..603c6e8e7 100644 --- a/readme.md +++ b/readme.md @@ -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 From 7ddd619668df381e39b20daaaec219d34d898fef Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sun, 19 Oct 2025 15:54:13 +0530 Subject: [PATCH 5/9] doc: improve code comments --- source/array-at.d.ts | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/source/array-at.d.ts b/source/array-at.d.ts index 0271ee19f..cb335670b 100644 --- a/source/array-at.d.ts +++ b/source/array-at.d.ts @@ -78,26 +78,26 @@ Recursion order for `ArrayAtPositiveIndex<["a", "b", ...number[], "c", "d", "e"] 4. `ArrayAtPositiveIndex<["c", "d", "e"], 0, 2, number | "c">` // Match found, `Right` not yet `0`, decrement `Right`, add current element to result. 5. `ArrayAtPositiveIndex<["d", "e"], 0, 1, number | "c" | "d">` // Match found, `Right` not yet `0`, decrement `Right`, add current element to result. 6. `ArrayAtPositiveIndex<["e"], 0, 0, number | "c" | "d" | "e">` // Match found, `Right` is `0`, add current element to result and return result. -7. Result: `number | "c" | "d" | "e"` + +Result: `number | "c" | "d" | "e"` */ type ArrayAtPositiveIndex = TArray extends readonly [] - ? Result | undefined // If the array is exhausted, and `Index` hasn't been found yet, return `Result` with `undefined`. + ? Result | undefined // If the array is exhausted, return `Result` with `undefined`. : keyof TArray & `${number}` extends never // Enters this branch, if `TArray` is empty (e.g., `[]`), // or `TArray` contains no non-rest elements preceding the rest element (e.g., `[...string[]]` or `[...string[], string]`). ? ExcludeRestElement extends infer TWithoutRest extends UnknownArray - // Remove the rest element and recurse further with the elements AFTER the rest element, + // Remove the rest element and recurse further with the elements after the rest element, // Set `Index` to `0` & `Right` to `Index`, so that elements keep getting matched until `Right` reaches `0`. - // Also, add the rest element to `Result`. - ? ArrayAtPositiveIndex> + ? ArrayAtPositiveIndex> // Also, add the rest element to `Result`. : never // Should never happen : TArray extends readonly [(infer First)?, ...infer Rest] ? Index extends 0 ? Right extends 0 - ? Result | First | If, undefined, never> // If there's a match, and `Right` is `0`, return `Result` with the current element. - : ArrayAtPositiveIndex, Result | First> // Enters this branch for elements after the rest element, here we add the current element to the result and decrement `Right`. - : ArrayAtPositiveIndex, Right, Result> // Enters this branch for elements before the rest element, here we decrement `Index` and recurse further. + ? Result | First | If, undefined, never> // If there's a match, and `Right` is `0`, return `Result` with `First`. + : ArrayAtPositiveIndex, Result | First> // Enters this branch for elements after the rest element, so `First` cannot be optional here. + : ArrayAtPositiveIndex, Right, Result> // Enters this branch for elements before the rest element. : never; // Should never happen /** @@ -109,24 +109,35 @@ Recursion order for `ArrayAtNegativeIndex<["a", "b", "c", ...number[], "d", "e"] 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. -7. Result: "a" | "b" | "c" | number + +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` (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 */ type ArrayAtNegativeIndex = TArray extends readonly [] - ? Result | undefined // If the array is exhausted, and `Index` hasn't been found yet, return `Result` with `undefined`. + ? 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, Left, Result> // Otherwise, decrement `Index`. + : ArrayAtNegativeIndex, Left, Result> // Enters this branch, if `TArray` contains no elements after the rest element. : ExcludeRestElement extends infer TWithoutRest extends UnknownArray - // Remove the rest element and recurse further with the elements BEFORE the rest element, + // Remove the rest element and recurse further with the elements before the rest element, // Set `Index` to `-1` & `Left` to `Sum`, so that elements keep getting matched until `Left` reaches `0`. - // Also, add the rest element to `Result`. - ? ArrayAtNegativeIndex, ExtractRestElement | Result> + ? ArrayAtNegativeIndex, ExtractRestElement | 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 @@ -135,8 +146,8 @@ type ArrayAtNegativeIndex, Last | Result> // If there's a match, and `Left` is not `0`, increment `Left` and add `Last` to `Result`. : ArrayAtNegativeIndex // If `Last` is optional, just add it to `Result` without changing anything else. : TArray extends readonly [...infer Rest, unknown] - ? ArrayAtNegativeIndex, Left, Result> // If current element is not optional, just increment `Index`. - : ArrayAtNegativeIndex, Subtract, Result> // If current element is optional, increment `Index` and decrement `Left`. + ? ArrayAtNegativeIndex, Left, Result> // If `Last` is not optional, just increment `Index`. + : ArrayAtNegativeIndex, Subtract, Result> // If `Last` is optional, increment `Index` and decrement `Left`. : never; // Should never happen export {}; From 809e6a4bef1feeddd3b1d74f401e440313cdf24b Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sun, 19 Oct 2025 16:13:03 +0530 Subject: [PATCH 6/9] refactor: update outcome based on `exactOptionalPropertyTypes` flag --- source/array-at.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/array-at.d.ts b/source/array-at.d.ts index cb335670b..e1e802f82 100644 --- a/source/array-at.d.ts +++ b/source/array-at.d.ts @@ -1,6 +1,7 @@ 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 {IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts'; import type {IsOptionalKeyOf} from './is-optional-key-of.d.ts'; import type {IsNegative} from './numeric.d.ts'; import type {Subtract} from './subtract.d.ts'; @@ -144,7 +145,7 @@ type ArrayAtNegativeIndex, Last | Result> // If there's a match, and `Left` is not `0`, increment `Left` and add `Last` to `Result`. - : ArrayAtNegativeIndex // If `Last` is optional, just add it to `Result` without changing anything else. + : ArrayAtNegativeIndex | Result> // If `Last` is optional, just add it to `Result` without changing anything else. : TArray extends readonly [...infer Rest, unknown] ? ArrayAtNegativeIndex, Left, Result> // If `Last` is not optional, just increment `Index`. : ArrayAtNegativeIndex, Subtract, Result> // If `Last` is optional, increment `Index` and decrement `Left`. From d9bbad735f0012227a6389f14d6d36f3520ef0f1 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sun, 19 Oct 2025 16:13:42 +0530 Subject: [PATCH 7/9] doc: improve code comments --- source/array-at.d.ts | 54 +++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/source/array-at.d.ts b/source/array-at.d.ts index e1e802f82..af8bab902 100644 --- a/source/array-at.d.ts +++ b/source/array-at.d.ts @@ -71,16 +71,16 @@ export type ArrayAt = : never; // Should never happen /** -Recursion order for `ArrayAtPositiveIndex<["a", "b", ...number[], "c", "d", "e"], 4>`: +Recursion order for `ArrayAtPositiveIndex<['a', 'b', ...number[], 'c', 'd', 'e'], 4>`: -1. `ArrayAtPositiveIndex<["a", "b", ...number[], "c", "d", "e"], 4>` // No match, decrement `Index`. -2. `ArrayAtPositiveIndex<["b", ...number[], "c", "d", "e"], 3, 0, never>` // No match, decrement `Index`. -3. `ArrayAtPositiveIndex<[...number[], "c", "d", "e"], 2, 0, number>` // Found rest element, set `Index` to `0`, `Right` to `Index` (i.e., `2`), add rest element to result. -4. `ArrayAtPositiveIndex<["c", "d", "e"], 0, 2, number | "c">` // Match found, `Right` not yet `0`, decrement `Right`, add current element to result. -5. `ArrayAtPositiveIndex<["d", "e"], 0, 1, number | "c" | "d">` // Match found, `Right` not yet `0`, decrement `Right`, add current element to result. -6. `ArrayAtPositiveIndex<["e"], 0, 0, number | "c" | "d" | "e">` // Match found, `Right` is `0`, add current element to result and return result. +1. `ArrayAtPositiveIndex<['a', 'b', ...number[], 'c', 'd', 'e'], 4>` // No match, decrement `Index`. +2. `ArrayAtPositiveIndex<['b', ...number[], 'c', 'd', 'e'], 3, 0, never>` // No match, decrement `Index`. +3. `ArrayAtPositiveIndex<[...number[], 'c', 'd', 'e'], 2, 0, number>` // Found rest element, set `Index` to `0`, `Right` to `Index` (i.e., `2`), add rest element to result. +4. `ArrayAtPositiveIndex<['c', 'd', 'e'], 0, 2, number | 'c'>` // Match found, `Right` not yet `0`, decrement `Right`, add current element to result. +5. `ArrayAtPositiveIndex<['d', 'e'], 0, 1, number | 'c' | 'd'>` // Match found, `Right` not yet `0`, decrement `Right`, add current element to result. +6. `ArrayAtPositiveIndex<['e'], 0, 0, number | 'c' | 'd' | 'e'>` // Match found, `Right` is `0`, add current element to result and return result. -Result: `number | "c" | "d" | "e"` +Result: `number | 'c' | 'd' | 'e'` */ type ArrayAtPositiveIndex = TArray extends readonly [] @@ -102,26 +102,38 @@ type ArrayAtPositiveIndex`: +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` (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. +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` (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 +Result: `'a' | 'b' | 'c' | number` --- -Recursion order for `ArrayAtNegativeIndex<["a", "b", "c"?, ...number[]], -1>`: +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` (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. +1. `ArrayAtNegativeIndex<['a', 'b', 'c'?, ...number[]], -1, 0, never>` // Found rest element, set `Index` to `-1`, `Left` to `Sum` (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 +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 readonly [] From 7a18ccf42cf98f6b5a2f52dec34f59fdf6f0dff5 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Sun, 19 Oct 2025 17:02:35 +0530 Subject: [PATCH 8/9] refactor: significantly simplify implementation by removing looping --- source/array-at.d.ts | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/source/array-at.d.ts b/source/array-at.d.ts index af8bab902..a15778dba 100644 --- a/source/array-at.d.ts +++ b/source/array-at.d.ts @@ -1,11 +1,15 @@ 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'; /** @@ -70,36 +74,12 @@ export type ArrayAt = : never // Should never happen : never; // Should never happen -/** -Recursion order for `ArrayAtPositiveIndex<['a', 'b', ...number[], 'c', 'd', 'e'], 4>`: - -1. `ArrayAtPositiveIndex<['a', 'b', ...number[], 'c', 'd', 'e'], 4>` // No match, decrement `Index`. -2. `ArrayAtPositiveIndex<['b', ...number[], 'c', 'd', 'e'], 3, 0, never>` // No match, decrement `Index`. -3. `ArrayAtPositiveIndex<[...number[], 'c', 'd', 'e'], 2, 0, number>` // Found rest element, set `Index` to `0`, `Right` to `Index` (i.e., `2`), add rest element to result. -4. `ArrayAtPositiveIndex<['c', 'd', 'e'], 0, 2, number | 'c'>` // Match found, `Right` not yet `0`, decrement `Right`, add current element to result. -5. `ArrayAtPositiveIndex<['d', 'e'], 0, 1, number | 'c' | 'd'>` // Match found, `Right` not yet `0`, decrement `Right`, add current element to result. -6. `ArrayAtPositiveIndex<['e'], 0, 0, number | 'c' | 'd' | 'e'>` // Match found, `Right` is `0`, add current element to result and return result. - -Result: `number | 'c' | 'd' | 'e'` -*/ -type ArrayAtPositiveIndex = - TArray extends readonly [] - ? Result | undefined // If the array is exhausted, return `Result` with `undefined`. - : keyof TArray & `${number}` extends never - // Enters this branch, if `TArray` is empty (e.g., `[]`), - // or `TArray` contains no non-rest elements preceding the rest element (e.g., `[...string[]]` or `[...string[], string]`). - ? ExcludeRestElement extends infer TWithoutRest extends UnknownArray - // Remove the rest element and recurse further with the elements after the rest element, - // Set `Index` to `0` & `Right` to `Index`, so that elements keep getting matched until `Right` reaches `0`. - ? ArrayAtPositiveIndex> // Also, add the rest element to `Result`. - : never // Should never happen - : TArray extends readonly [(infer First)?, ...infer Rest] - ? Index extends 0 - ? Right extends 0 - ? Result | First | If, undefined, never> // If there's a match, and `Right` is `0`, return `Result` with `First`. - : ArrayAtPositiveIndex, Result | First> // Enters this branch for elements after the rest element, so `First` cannot be optional here. - : ArrayAtPositiveIndex, Right, Result> // Enters this branch for elements before the rest element. - : never; // Should never happen +type ArrayAtPositiveIndex = + SplitOnRestElement extends readonly [infer BeforeRest extends UnknownArray, infer Rest extends UnknownArray, infer AfterRest extends UnknownArray] + ? LessThan['length']> extends true + ? BeforeRest[Index] + : Rest[number] | AfterRest[IntRange<0, Sum['length']>, 1>>] + : never; // Should never happen /** Recursion order for `ArrayAtNegativeIndex<['a', 'b', 'c', ...number[], 'd', 'e'], -5>`: From 001317f0cd31cbe636b35fe4c65834bb22977346 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Tue, 24 Mar 2026 20:32:39 +0530 Subject: [PATCH 9/9] refactor: simplify negative index handling logic --- source/array-at.d.ts | 82 +++++++++----------------------------------- 1 file changed, 17 insertions(+), 65 deletions(-) diff --git a/source/array-at.d.ts b/source/array-at.d.ts index a15778dba..33041400f 100644 --- a/source/array-at.d.ts +++ b/source/array-at.d.ts @@ -1,15 +1,13 @@ -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 {NumberAbsolute} from './internal/numeric.d.ts'; import type {IsExactOptionalPropertyTypesEnabled} from './internal/type.d.ts'; -import type {IsOptionalKeyOf} from './is-optional-key-of.d.ts'; +import type {LessThanOrEqual} from './less-than-or-equal.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'; /** @@ -81,66 +79,20 @@ type ArrayAtPositiveIndex = : Rest[number] | AfterRest[IntRange<0, Sum['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` (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` (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 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, Left, Result> - // Enters this branch, if `TArray` contains no elements after the rest element. - : ExcludeRestElement 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`, so that elements keep getting matched until `Left` reaches `0`. - ? ArrayAtNegativeIndex, ExtractRestElement | 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, Last | Result> // If there's a match, and `Left` is not `0`, increment `Left` and add `Last` to `Result`. - : ArrayAtNegativeIndex | Result> // If `Last` is optional, just add it to `Result` without changing anything else. - : TArray extends readonly [...infer Rest, unknown] - ? ArrayAtNegativeIndex, Left, Result> // If `Last` is not optional, just increment `Index`. - : ArrayAtNegativeIndex, Subtract, Result> // If `Last` is optional, increment `Index` and decrement `Left`. - : never; // Should never happen +type ArrayAtNegativeIndex = + SplitOnRestElement extends readonly [infer BeforeRest extends UnknownArray, infer Rest extends UnknownArray, infer AfterRest extends UnknownArray] + ? LessThanOrEqual, AfterRest['length']> extends true + ? AfterRest[Sum] + : Subtract, AfterRest['length']>> + >, 1> extends infer Indices extends number + ? + | Rest[number] + | Required[Indices] + | (true extends IsNegative ? undefined : never) + | If + : never + : never; // Should never happen export {};