How to create a union type of indexes of a constant array with Typescript?
Asked Answered
N

4

7

I have a constant array of strings e.g.

const emojis = ['πŸ˜„', '😊', '😐', 'πŸ˜•', '😣'] as const

And I want to have a type that contains a union of the indexes of that array e.g.

type emojiIndexes = IndexesOfArray<typeof emojis> // => 0 | 1 | 2 | 3 | 4

So that I don't allow using number and use only the exact number of the indexes in the array

And if the array size e.g.

// changed from this
// const emojis = ['πŸ˜„', '😊', '😐', 'πŸ˜•', '😣'] as const
// to this 
const emojis = ['πŸ˜„', '😊', '😐'] as const // removed 2 emojis

Than, IndexesOfArray<typeof emojis> would be 0 | 1 | 2

How can I create IndexesOfArray that would create a union type with the indexes of the constant array?

Narcolepsy answered 24/3, 2021 at 17:18 Comment(0)
M
4

Here is a solution: (Playground Link)

type TupleIndices<A extends any[]>
    = A extends [any, ...infer T]
    ? TupleIndices<T> | T['length']
    : never

Example:

type Foo = ['foo', 'bar', 'baz', 'qux', 'quz']

// 0 | 4 | 3 | 2 | 1
type FooIndices = TupleIndices<Foo>

Because the solution is recursive, it will fail for moderately long tuples. If you need this to work for longer tuples, you can try a tail-recursive version: (Playground Link)

type TupleIndices<A extends any[], Acc = never>
    = A extends [any, ...infer T]
    ? TupleIndices<T, Acc | T['length']>
    : Acc

Usage is the same.

Monotint answered 24/3, 2021 at 17:31 Comment(3)
This fails on more than 23 elements though due to the recursion limit. – Benzofuran
@Benzofuran Yes, that's likely, but I doubt there is anything better - at least until Typescript introduces support for number range types (see this suggestion on the issue tracker). In general, you can't use Typescript's type system to do "too much" computation at compile-time. – Monotint
True, I usually just use the union of strings, haven't had any issues with it so far. – Benzofuran
B
4

You can do this by excluding all empty-array keys from the parameter type, so you end up with a union of just the indices:

type IndexesOfArray<A> = Exclude<keyof A, keyof []>

const emojis = ['πŸ˜„', '😊', '😐', 'πŸ˜•', '😣'] as const

type emojiIndexes = IndexesOfArray<typeof emojis> // => '0' | '1' | '2' | '3' | '4'

The indices are strings instead of numbers but that should not cause any issues. If you do want numbers, you could use a recursive conditional type to generate them, but this will cause issues with TypeScript's recursion depth. Alternatively, you can use a slightly hacky hard-coded array and index that to get the numbers:

type ToNum = [0,1,2,3,4,5,6,7] // add as many as necessary

type emojiNumIndexes = ToNum[IndexesOfArray<typeof emojis>] // => 0 | 1 | 2 | 3 | 4

TypeScript playground

Benzofuran answered 24/3, 2021 at 17:24 Comment(2)
Perfect! But is it possible to make it into numbers? the keyof A are string – Narcolepsy
Ah yes, was just typing a line about that. I don't think you can easily get numbers. Let me have a look. – Benzofuran
M
4

Here is a solution: (Playground Link)

type TupleIndices<A extends any[]>
    = A extends [any, ...infer T]
    ? TupleIndices<T> | T['length']
    : never

Example:

type Foo = ['foo', 'bar', 'baz', 'qux', 'quz']

// 0 | 4 | 3 | 2 | 1
type FooIndices = TupleIndices<Foo>

Because the solution is recursive, it will fail for moderately long tuples. If you need this to work for longer tuples, you can try a tail-recursive version: (Playground Link)

type TupleIndices<A extends any[], Acc = never>
    = A extends [any, ...infer T]
    ? TupleIndices<T, Acc | T['length']>
    : Acc

Usage is the same.

Monotint answered 24/3, 2021 at 17:31 Comment(3)
This fails on more than 23 elements though due to the recursion limit. – Benzofuran
@Benzofuran Yes, that's likely, but I doubt there is anything better - at least until Typescript introduces support for number range types (see this suggestion on the issue tracker). In general, you can't use Typescript's type system to do "too much" computation at compile-time. – Monotint
True, I usually just use the union of strings, haven't had any issues with it so far. – Benzofuran
M
0

We can leverage the fact that keyof an array yields a union of all methods available on an array, like push, concat, map etc and if the array is of fixed length (i.e. a tuple) it also adds the indices into this union. So, we can extract just the indices from the union using Exclude.

type Indices<TArr> = Exclude<keyof TArr, keyof []>;

But we might also want to add a proper constraint to TArr that would ensure that we only pass fixed length arrays to Indices.

type Indices<TArr extends readonly [] | [any, ...any]> = Exclude<keyof TArr, keyof []>;
Melodiemelodion answered 23/7, 2023 at 9:51 Comment(0)
H
0

There's a simpler version here that comes out to this, if you document how the cleverness works:

/** Given a const array, return the union of all its numeric indices */
export type ArrayIndices<T extends readonly unknown[]> = Exclude<
    Partial<T>["length"], // the union of all array lengths up to the length of the T array
    T["length"] // …save that last, which isn't indexable, as last indexable element is n-1
>;

I e:

const emojis = ['πŸ˜„', '😊', '😐', 'πŸ˜•', '😣'] as const;
type indices = ArrayIndices<typeof emojis>;
//   ^? type indices = 0 | 1 | 2 | 3 | 4
Hillside answered 13/2, 2024 at 23:25 Comment(0)

© 2022 - 2025 β€” McMap. All rights reserved.