(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!
Top comments (9)
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 ofPrev<T>
! Check it out:Interesting I like the way you removed the first item from the tuple.
It can be simplified more by doing this:
This magic spell has just saved me a bunch of headache. Thanks, Keegan!
For the
GetLength
type, you can actually just access thelength
property via bracket notation and receive the same result: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 originalGetLength
type.We don't have to rely on getting the length. Use tuple destructuring for a simple one liner:
A simpler way to find the
n-1
Hey! Great stuff. Desperately need new line here
nevertype GetLast
betweennever
andtype
.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']>