loading...

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

miracleblue profile image Nicholas Kircher ・1 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]

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

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

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

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!

Posted on Sep 2 '18 by:

miracleblue profile

Nicholas Kircher

@miracleblue

I have 2 beautiful girls, and I am a staunch advocate for Functional Programming. I also am a bit of a nervous wreck most of the time.

Discussion

markdown guide
 

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>>];
 

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

 

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

 

I came just for GetLast 🤣

 

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

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