Introduction
So, here's a wild thought! What if I told you that we can use JS array functions like pop
, push
, shift
, unshift
with Typescript tuple type? You might be like, why…why would we even need that? Isn't TS already hard enough? Who on earth would want to make it even more complex?
Look, I did warn you that this is a bit out there. I've kinda become a Typescript fanatic recently and I love digging into some bizarre TS stuff. Plus, it's not just for kicks. It also helps us understand a few crucial TS concepts that can come in handy when crafting your own utilities.
So let me grab your attention and share with you what we are gonna be doing.
💡 NOTE: This blog post is totally inspired by Typescript challenges here: https://github.com/type-challenges/type-challenges?tab=readme-ov-file The utilities mentioned in this blog were part of these challenges.
What are we gonna do
Its simple we are building typescript utility generics that does same operations as functions of an Array like push pop etc.
Here are some examples,
type Pop<T extends unknown[]> = T extends [...infer H, unknown] ? H : []
type Shift<T extends unknown[]> = T extends [unknown, ...infer T] ? T : []
type Push<T extends unknown[], I extends unknown> = [...T, I]
type Unshift<T extends unknown[], I extends unknown> = [I, ...T]
type popRes = Pop<[3, 2, 1]>
type shiftRes = Shift<[3, 2, 1]>
type pushRes = Push<[3,2,1], 0>
type unshiftRes = Unshift<[3,2,1], 4>
Getting Started
Before we dive into the implementation of these utilities we first need to understand couple of basic things in TypeScript:
1. Tuple Types
A tuple type is similar to an Array
type which has known types at the specified index position. A typical example of tuple would like below:
type TupleType = [number, boolean];
Here TupleType
is a tuple type with number
type being at position 0
and boolean
type at position 1
. Analogous to Javascript arrays, if you try to index tuple type out of its bounds we will get an error
type res = TupleType[2] // <------ Tuple type 'TupleType' of length '2' has no element at index '2'
For more information on typescript tuple type read its documentation here: TS Tuple Types
2. Variadic Tuples
Variadic tuples were introduced in Typescript via this PR. According to the PR, variadic tuples are:
The ability for tuple types to have spreads of generic types that can be replaced with actual elements through type instantiation
Some examples of variadic tuples are as follows:
type WrapWithStringAndBool<T extends Array<unknown>> = [string, ...T, boolean];
type res = WrapWithStringAndBool<[1,2,3,4]> // <-- [string, 1, 2, 3, 4, boolean]
If you checkout the above example, we have WrapWithStringAndBool
generic that accepts an array of unknowns and returns a tuple with string
and boolean
tupes at the first and the last position in the tuple.
Here the ...T
is the variadic element which acts as a placeholder for values from the input T
.
Variadic tuples has a lot to offer and is a great piece of feature. I would like recommend to go through its rules and examples mentioned in this PR.
3. Conditional Types
Every developer knows the if
else
block that helps you to write conditional code. In the similar way, Typescript conditional types take the following format: condition ? trueBranch : falseBranch
. This is very much similar to the ternary operator in JS.
For a type to become conditional, the condition
part should consist of extends
keyword:
Type extends OtherType ? trueBranch : falseBranch
Were Type
if matches with OtherType
then trueBranch
is executed or else the falseBranch
.
Conditional Types can also be used to narrow the types to provide specific types. Consider the following example from Typescript cheatsheet:
type Bird = { legs: 2 };
type Dog = { legs: 4 };
type Ant = { legs: 6 };
type Wolf = { legs: 4 };
type HasFourLegs<Animal> = Animal extends { legs: 4 } ? Animal : never;
type Animals = Bird | Dog | Ant | Wolf;
type FourLegs = HasFourLegs<Animals>; // <--- Dog | Wolf
Here we have 4 types Bird
Dog
Ant
and Wolf
with their respective legs property. Next, we have built HasFourLegs
. It does the following thing:
- It compares
Animal
object with{ legs: 4 }
object with the help ofextends
keyword. - If they are equal,
Animal
is returned or else nothing is returned.
In the last line we make use of HasFourLegs
by passing it the Animals
union type. A thing to note here is that, when an Union type is provided to the left side of the extends
keyword then all the values of Union are distributed such that each value is tested against the object { legs: 4 }
. You can read more about the distributive nature of unions over here.
4. Inferring Within Conditional Types
Conditional types can also be used to infer types when we do comparison with the help of infer
keyword. For example, consider the TS utility ReturnType. This utility returns the return type of the function that is being passed to it. Internally it works in the following ways:
type GetReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;
type res = GetReturnType<() => number> // <--- number
In the trueBranch
of the above example if you closely observe that we make use of infer R
. Here we tell TS like while comparing if T
matches the format to the right of extends
then place the return type of function in R
.
When we pass () => number
to the GetReturnType
a number is returned since it matches the format to the right of extends
keyword in the generic.
Now that we are clear with our basics, let us start with writing our first utility. Pop
Pop
In Javascript a pop
function would remove the last element of an array. To build this in Typescript, we do the following:
type Pop<T extends unknown[]> = T extends [...infer H, unknown] ? H : []
Let me example what is happening here:
- We tell TS that
Pop
is going to be our generic with parameterT
which can be an array ofunknown
s. - Next, with the help of this:
T extends [...infer H, unknown]
we tell TS that the first N-1 elements of the tuple/array T should be inferred with H and the Nth i.e. the last element should be inferred as unknown. - If the Tuple T matches this pattern then
H
is returned or else we return an empty array.
Shift
Javascript array’s has another built-in function that helps you to remove its first element. This is an in-place operation that affects the original array. The returned value by this function is an array’s first element. Here is a quick look of this function:
let x = [1, 2, 3, 4];
const firstElement = x.shift();
firstElement // <----- 1
x // <----- [2, 3, 4]
Similar to shift
function, we can create a generic utility in Typescript that removes the first element from a tuple type. Here is how it’s the utility will look like:
type Shift<T extends unknown[]> = T extends [unknown, ...infer R] ? R : []
The Shift
utility here expects it’s input to be an tuple. In this case, since we don’t exactly know the types inside the tuple hence we extend input T
as unkown[]
To the right hand side, we have:
- We are making using of conditional types, were we check if
T
matches the following pattern[unknown, ...infer R]
.- We can also identify the things that we learned in the getting started section.
- We make use of
extends
keyword to create a conditional types. - We also make use of variadic tuple types, where in the right side tuple of
extends
we have a inferential variadic elementR
- If the condition is met we return
R
which is the remaining element of the tuple.
Push & Unshift
Similar to Javascript’s push built-in function that appends a new element to the array, we can also create a similar utility:
type Push<T extends unknown[], I extends unknown> = [...T, I]
This is one of the simplest utility from all. The key aspect of this Utility is that,
- We make use of destructing of tuple concept with
...T
inside a new tuple. - To this tuple at the end we add
I
- Together this utility would return a new tuple type that combines the elements from
T
and addsI
at the end.
Here is the result for the same:
type res = Push<[1,2,3], 4>
res // <----- [1, 2, 3, 4]
Similar to this we have Unshift
as well:
type Unshift<T extends unknown[], I extends unknown> = [I, ...T]
Summary
In this blog post, we covered:
- Tuple Types
- Conditional Types
- Variadic Tuple Types
- Conditionally Inferred Types
Lastly, we explored the Push
, Pop
, Shift
, and Unshift
generic utilities in TypeScript, which are analogous to their JavaScript counterparts.
Thanks a lot for reading my blogpost.
Top comments (2)
This is fantastic! I've always found TypeScript’s advanced type utility tricky—your explanations make it much more clear. How did you come across the inspiration for this post?
Thanks for the read @chariebee. I found the inspiration for this post when I was solving the typescript challenge