There is no straightforward method to achieve this, which can be quite frustrating. While there are ongoing efforts to address this issue, the current workaround involves a bit of extra work.
In my own project, I have implemented the following solution:
/** Define the type of elements in an array */
type ElementOf<T> = T extends (infer E)[] ? E : T extends readonly (infer E)[] ? E : never;
/** Function signature with arguments for 'Tail' */
type AsFunctionWithArgsOf<T extends unknown[] | readonly unknown[]> = (...args: T) => any;
/** Extract arguments from the function signature for 'Tail' */
type TailArgs<T> = T extends (x: any, ...args: infer T) => any ? T : never;
/** Obtain elements of an array after the first element */
type Tail<T extends unknown[] | readonly unknown[]> = TailArgs<AsFunctionWithArgsOf<T>>;
/** Internal utility for 'IndicesOf'; likely not needed outside this context */
type AsDescendingLengths<T extends unknown[] | readonly unknown[]> =
[] extends T ? [0] :
[ElementOf<ElementOf<AsDescendingLengths<Tail<T>>[]>>, T['length']];
/** Union of numeric literals representing possible indices of a tuple */
type IndicesOf<T extends unknown[] | readonly unknown[]> =
number extends T['length'] ? number :
[] extends T ? never :
ElementOf<AsDescendingLengths<Tail<T>>>;
By using this implementation, your code can be structured as follows:
const list = ['foo', 'bar', 'baz'] as const;
type ListIndex = IndicesOf<typeof list>;
The underlying concept here revolves around examining the `T['length']` property (where `type T = typeof list`) to obtain actual numbers that correlate with the indices. The extracted number will always be one greater than the highest index in the array. By iteratively removing elements from the array and checking the length each time, we can determine all the indices present. This recursive process relies on the definitions of types like `Tail`, `ElementOf`, and `AsDescendingLengths`.
To elaborate further, `Tail` represents the same tuple type without its initial element, akin to a 'Pop'. Due to TypeScript's limited functionality with tuples, we utilize a function-based approach for manipulating them effectively. Functions like `ElementOf` and `Tail` serve other purposes beyond this specific scenario. However, caution must be exercised with deeply nested tuples, as TypeScript may avoid recursive types for performance reasons.
Although certain constructs like `AsDescendingLengths` may seem tailored for this particular use case, these defined types could potentially find relevance in broader contexts. Including these definitions in a global `.d.ts` file eliminates the need for individual imports but might clutter the overall scope of the project.
An additional application example is demonstrated below:
interface Array<T> {
/**
* Returns the index of the first occurrence of a value in an array.
* @param searchElement The value to locate in the array.
* @param fromIndex The array index at which to begin the search.
* If `fromIndex` is omitted, the search starts at index 0.
* Returns -1 if `searchElement` is not found.
*/
indexOf<This extends T[], T>(this: This, searchElement: T, fromIndex?: IndicesOf<This>): -1 | IndicesOf<This>;
}
interface ReadonlyArray<T> {
/**
* Returns the index of the first occurrence of a value in an array.
* @param searchElement The value to locate in the array.
* @param fromIndex The array index at which to begin the search.
* If `fromIndex` is omitted, the search starts at index 0.
* Returns -1 if `searchElement` is not found.
*/
indexOf<This extends readonly T[], T>(this: This, searchElement: T, fromIndex?: IndicesOf<This>): -1 | IndicesOf<This>;
}