DEV Community

Keyur Paralkar
Keyur Paralkar

Posted on

Mastering TypeScript: Implementing Push, Pop, Shift, and Unshift in Tuples

Introduction

A wild thought image

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>
Enter fullscreen mode Exit fullscreen mode

Getting Started

Let us get started image

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];
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 of extends 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
Enter fullscreen mode Exit fullscreen mode

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

Pop in TS image

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 : []
Enter fullscreen mode Exit fullscreen mode

Let me example what is happening here:

  • We tell TS that Pop is going to be our generic with parameter T which can be an array of unknowns.
  • 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

Shift in TS image

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]
Enter fullscreen mode Exit fullscreen mode

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 : []
Enter fullscreen mode Exit fullscreen mode

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 element R
  • If the condition is met we return R which is the remaining element of the tuple.

Push & Unshift

Shift in TS image

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]
Enter fullscreen mode Exit fullscreen mode

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 adds I at the end.

Here is the result for the same:

type res = Push<[1,2,3], 4>

res // <----- [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Similar to this we have Unshift as well:

type Unshift<T extends unknown[], I extends unknown> = [I, ...T] 
Enter fullscreen mode Exit fullscreen mode

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.

You can follow me on twittergithub, and linkedIn.

Top comments (2)

Collapse
 
chariebee profile image
Charles Brown

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?

Collapse
 
keyurparalkar profile image
Keyur Paralkar

Thanks for the read @chariebee. I found the inspiration for this post when I was solving the typescript challenge