DEV Community

loading...

How 2 TypeScript: Get the last item type from a tuple of types

miracleblue profile image Nicholas Kircher ・4 min read

(Note: The author assumes you're using TypeScript 3.x)

Hey you! Do you want to get the type of the last item from a tuple of types?

Of course you do.

type Stuff = [number, boolean, string]
type OtherStuff = [string, number, object, boolean]
Enter fullscreen mode Exit fullscreen mode

Here I have 2 tuple types, one with 3, and one with 4 items. The last type in Stuff is string, and the last type in OtherStuff is boolean.

How do we get those last item types?

Well, amazingly, one way we can do it - if we know the length of the tuple at time of writing - is to use a numeric literal as an index for looking up the type at a position. I know, amazing. Like so:

type LastStuff = Stuff[2] // => string
type LastOtherStuff = OtherStuff[3] // => boolean
Enter fullscreen mode Exit fullscreen mode

Kinda like a normal array lookup!

But what if you don't know the length of the tuple? Hmm... how do we get TypeScript to tell us the length and then let us use that length to pick out the last item, all at compile time?

Borrowing from this amazing HKTs library I was able to get the length of the tuple as a numeric literal:

type GetLength<original extends any[]> = original extends { length: infer L } ? L : never

type Stuff = [number, boolean, string]
type OtherStuff = [string, number, object, boolean]

type LengthStuff = GetLength<Stuff> // => 3
Enter fullscreen mode Exit fullscreen mode

Notice the infer keyword in this part: original extends { length: infer L } - I talk more about what the infer keyword is in the previous How 2 TypeScript post, so if you're confused, I hope that sheds a bit of light :)

But remember, lists are zero-indexed, so if we want the last item, we will need a way to do L - 1. We can't just do straight arithmetic at the type level in TS (yet), so this does not work: type LastItem = Stuff[GetLength<Stuff> - 1] (thats a syntax error for TS). So we're gonna need a map of some kind.

The approach I felt would be best was a type mapping from a numeric literal to the previous numeric literal. I found just such a type from this SimplyTyped library, which is essentially giving us n - 1 for anything between 0 and 63. Using this, I can input the length we inferred as L and get back the last index of that list, and use that to look up the last type. Like so:

// Borrowed from SimplyTyped:
type Prev<T extends number> = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62][T];

// Actual, legit sorcery
// Borrowed from pelotom/hkts:
type GetLength<original extends any[]> = original extends { length: infer L } ? L : nevertype GetLast<original extends any[]> = original[Prev<GetLength<original>>]

// Here are our test-subject tuples:
type Stuff = [number, boolean, string]
type OtherStuff = [string, number, object, boolean]

// How long is the `Stuff` tuple?
type LengthStuff = GetLength<Stuff> // => 3

// What is the last element of each tuple?
type LastStuff = GetLast<Stuff> // => string
type LastOtherStuff = GetLast<OtherStuff> // => boolean
Enter fullscreen mode Exit fullscreen mode

Boom! It's not perfect, but this is about as good as I could make it with the latest TypeScript version.

After thoughts:

I actually tried many, many different things to make this work. The first version that I got working used the "TupleTable" from the HKTs library to return to me the inferred type of the last item, using a lookup based on the length of the tuple. That felt a bit overkill for this task, and was limited to only 10 items. If I wanted to increase the limit, that tuple table would have to get much, much bigger.

Instead, I looked around for a way to do a bit of type-level arithmetic. If we say that any given tuple's length is n, then the last item of any given tuple is at n - 1. We know this from the countless times we've had to lookup the last item in an array in JS. In TypeScript, we can't (yet) do any arithmetic operations on numeric literal types natively, so if we go down this road, we will need a kind of mapping between numeric literals (so that given 5, we get back 4). It will also be of a finite length, but hopefully the complexity of the code won't increase significantly if we increase the max length by 1 (for example).

After searching GitHub, I found exactly what I needed. The Prev type from SimplyTyped will let us pass in a number (anywhere from 0 to 63) and it will give us the number before it. So, given 5, you get back 4. Until we get a built-in "successor/predecessor" type in TypeScript, I think this is about as good as it gets.

I'm actually quite surprised at the power of TypeScript's type system, compared to what I'm used to in Haskell and PureScript. There are still many limitations in comparison, and perhaps there always will be, but one thing is certain: TypeScript can hold its own in this cut-throat world of typed languages.

Until next time!

Discussion (7)

pic
Editor guide
Collapse
kjleitz profile image
Keegan Leitz

Hey! Great article. In case it's useful, I figured out a way to get the length - 1 of an arbitrary-length tuple, so you're no longer limited to the hard-coded breadth of Prev<T>! Check it out:

// Gets the length of an array/tuple type. Example:
//
//   type FooLength = LengthOfTuple<[string, number, boolean]>;
//   //=> 3
//
export type LengthOfTuple<T extends any[]> = T extends { length: infer L } ? L : never;

// Drops the first element of a tuple. Example:
//
//   type Foo = DropFirstInTuple<[string, number, boolean]>;
//   //=> [number, boolean]
//
export type DropFirstInTuple<T extends any[]> = ((...args: T) => any) extends (arg: any, ...rest: infer U) => any ? U : T;

// Gets the type of the last element of a tuple. Example:
//
//   type Foo = LastInTuple<[string, number, boolean]>;
//   //=> boolean
//
//   function lastArg<T extends any[]>(...args: T): LastInTuple<T> {
//     return args[args.length - 1];
//   }
//
//   const bar = lastArg(1);
//   type Bar = typeof bar;
//   //=> number
//
//   const baz = lastArg(1, true, "hey", 123, 1, 2, 3, 4, 5, 6, 7, -1, false);
//   type Baz = typeof baz;
//   //=> boolean
//
export type LastInTuple<T extends any[]> = T[LengthOfTuple<DropFirstInTuple<T>>];
Enter fullscreen mode Exit fullscreen mode
Collapse
menocomp profile image
Mina Luke

Interesting I like the way you removed the first item from the tuple.
It can be simplified more by doing this:

type DropFirstInTuple<T extends any[]> = T extends [arg: any, ...rest: infer U] ? U : T;
Enter fullscreen mode Exit fullscreen mode
Collapse
mattmcmahon profile image
Matt McMahon

This magic spell has just saved me a bunch of headache. Thanks, Keegan!

Collapse
sno2 profile image
Carter Snook • Edited

For the GetLength type, you can actually just access the length property via bracket notation and receive the same result:

type GetLength<T extends unknown[]> = T["length"];
Enter fullscreen mode Exit fullscreen mode

I would recommend this method more because it looks like it's intrinsic behavior built within TypeScript to set that and I would assume it would also be faster and result in less type computations. Finally, it would also get rid of the never fail-safe in the original GetLength type.

Collapse
reggi profile image
Thomas Reggi

Hey! Great stuff. Desperately need new line here nevertype GetLast between never and type.

Collapse
imsergiobernal profile image
Sergio

I came just for GetLast 🤣

Collapse
safarishi profile image
石发磊

type Tt = ((...args: T) => any) extends ((f: any, ...rest: infer R) => any) ? T[R['length']] : never

type Res2 = Tt<['x', 'y']>