Pushing the limits of TypeScript

Pushing the limits of TypeScript

I often get stuck with TypeScript, and when I do, I want to throw my Mac out of the window. So I’ve created this article, to help you and I write better code. I’ll be updating this post when I stumbled upon new and useful TypeScript hacks. In the meantime, feel free to get inspired to create your own cool TS magic, or to just steal some code, I won’t judge… 😜

First index of array

You might want to get the type of the first index in an array, and if so, this utility type is for you.

type First<T extends any[]> = T extends [infer P, ...any[]] ? P : never;

It’s a rather simple utility type to help you infer the type of the first index of an array.

type Test1 = First<[() => string, "a", "b"]>;
//   ^ Test1: () => string

type Test2 = First<[undefined, 4]>;
//   ^ Test2: undefined

type Test3 = First<[]>;
//   ^ Test3: never

As a bonus, to get the last element of the tuple, change [infer P, ...any[]] to [...any[], infer P]!

If you’d like to use a variable of type tuple, you’ll be better off using the bracket notation to access the first element of the array, like this: T[0].

type First<T extends readonly any[]> = T[0];

And then pass the typeof your tuple.

const numbers = [1, 2, 3, 4, 5] as const;

type Test = First<typeof numbers>;
//   ^ Test: () => 1

Note: remember to cast your array as a const, using as const, TypeScript will then infer it as readonly [1, 2, 3, 4, 5].

Infer the type of each element when using map()

Let’s say you have an array of vowels, or string[]. Now, let’s say you want to loop over the array using Array.map(). You would have something like this:

const vowels = ["a", "e", "i", "o", "u"];

vowels.map((v: string) => {});
//          ^ v: string

An easy, yet underused approach is to cast the array to a tuple using as const. Doing so will allow Array.map() to return the possible values obtainable by Array.map(). So now, your code should lok something like this.

const vowels = ["a", "e", "i", "o", "u"] as const;

vowels.map((v) => {});
//          ^ v: "a" | "e" | "i" | "o" | "u"

Casting your array as a const does come with a drawback, however. You won’t be able to mutate it after the fact. That means no more Array.push(), Array.pop(), etc…

Getting the length of a tuple

TypeScript is really clever, as it lets us access the prototype of an element. So instead of regular JS, where we use Array.length, for TS Types, we can use any[]['length] to access the length. And if that array turns out to be a tuple, TS can directly infer the length!

type Length<T extends readonly any[]> = T["length"];

Using it is simple, either pass a tuple directly into the type…

type Test1 = Length<["a", "b", "c"]>;
//   ^ Test1: 3

Or, a more practical approach is to pass a variable of type tuple…

const letters = ["a", "b", "c"] as const;

type Test2 = Length<typeof letters>;
//   ^ Test2: 3

Reminder: the above method only works with variables of type tuple, so arrays unfortunately will not work…

Conclusion

That’s it! I hope this helped someone out there. I’ll be updating this post when I stumble upon some other fancy TS trick. In the meantime, you can check out this post about Bun. It’s worth the read; I promise.

Thanks for reading 😊