RemoveUndefined<T>, TS: Conditional Types & The infer Keyword

RemoveUndefined<T>,
TS: Conditional Types & The infer Keyword

Hey! In this blog, we are going to make a TypeScript type that can be used to remove undefined from an array type and in this adventure, we would discuss TypeScript Conditional Types and the infer keyword.

All the code used in this blog is available here.

Quick Aside on Conditional Types

We use conditional types when we need to return different types depending on a given condition. Think of it as a function that returns different outputs depending on a condition BUT instead of a function it is a type and instead of returning values, it returns types.

Lets quickly draw a parallel between conditional statements and conditional types.

Conditional Statements

const input = "yes";
const output = input === "yes" ? true : false;

Conditional Types

type input = "yes";
type output = input extends "yes" ? true : false;

The idea is simple, decide the type based on a condition. In the aforementioned example, we would get true as output type.

Lets move ahead and talk about extends keyword.

Quick Aside on extends Keyword

Loosely speaking, extends is type equivalent of === operator BUT there are some caveats that needs to be approached. Hence, putting it simply, we use extends to check if the type on the left is can be considered a subset of the type on the right. In the example mentioned above, we check if input type is a subset of literal type "yes" and clearly the answer is yes, it is, in-fact it is exactly the literal type "yes".

But then what do I mean by "subset", take for example we have:-

type output = number extends (string | number) ? true : false;

In this case, type number is NOT the exact type string | number, but is a subset of string | number, hence we would get true as our output type.

A simple way to evaluate an extends statement is by trying to answer the question, "If a variable passes for the type on the left of extends keyword can it also pass for the type on right?" and if the answer to this question is "yes" then the extends statement would evaluate to true.

So, in previous example, lets say we have a variable t and its value is 2 so this variable t passes for the type number which is the type on left, now can t pass for (string | number) type as well? The answer is yes, yes it can also pass for type (string | number) which means our extends statement would evaluate to true. But, if the type on the right would have been boolean then surely variable t whose value is 2 wont pass for type boolean, hence our extends statement would be evaluated to false in this case.

Another way we can use extends keyword is to restrict what can make its way as the type param in a generic type/class/interface. Lets understand it with an example.

Lets say we have a generic type MaxSpeed that returns the maxSpeed of a car, where we pass in the car type as a type param and we get the maxSpeed property of that particular car. So, lets code it out then.

Screenshot 2022-01-27 at 3.12.32 AM.png

What this error message says is the there is no such property as maxSpeed in type T hence we can not conclusively get a resultant type. Way to make this work is to make sure that T always have the property maxSpeed, this is where extends keyword would help us. So, lets say we restrict that any type param passed to MaxSpeed type would surely have property maxSpeed then it should work like a charm right?

type Car = {
    maxSpeed: number
}

type MaxSpeed<T extends Car> = T["maxSpeed"]

And surely, this works! This snippet basically says that T must extend type Car which means that it must be a subset of type Car and that in this case means, it must have a property maxSpeed and if thats the case, then we would be sure that there would exist a property maxSpeed in T which we would then use to conclusively decide upon a type.

Lets see what would happen if we pass in a type that does not extend Car in MaxSpeed generic type. Lets pass a type Laptop which is defined as follows:

type Laptop = {
   ram: number,
   cpus: number,
   processor: string,
}

And lets try to get MaxSpeedOfLaptop by passing Laptop as type param to generic type MaxSpeed

Screenshot 2022-01-28 at 12.20.29 AM.png

So, error message is pretty clear here, it says that:

"Hey! This type Laptop that you have passed to MaxSpeed does not satisfy the constraint as it does not have any property under the name of maxSpeed, therefore, I can not get a conclusive type for you. Try passing in a type that correctly extends type Car as it is a necessary constraint"

Wooff😅, that was a long "aside" on extends, although there is another property of extends and it is Distributivity, BUT we are not going to go in its details, it is very well summed up in TypeScript HandBook and I do not have anything to add to it and honestly I know you are getting bored so we are gonna skip that. Checkout this link if you wanna read up on the Distributive Conditional Types.

Perfect!! Now that we know 2 cents about Conditional types, lets get started on writing a generic type that would remove undefined from an Array type.

RemoveUndefined

So, lets approach this step by step.

  1. First, we need to make sure that only types that extend Array type makes it way in our RemoveUndefined type. Understandable right? If the type param passed is not an array type then how in the world are we going to remove undefined from this supposed array type.

  2. Second, now that we are sure that T is an array type, we need to check if any element inside it extends undefined. If yes, then we need to get rid of it and if not then great, we need not do anything.

Step One

This is gonna be simple, we just read on how to use extends keyword to restrict the type of type param that can make its way in a generic type, so lets use that here.

type RemoveUndefined<T extends any[]> = ...mystery... ;

Great! This restricts that T must be an array which is exactly what we need.

Step Two

Lets see what we discussed.

Second, now that we are sure that T is an array type, we need to check if any element inside it extends undefined. If yes, then we need to get rid of it and if not then great, we need not do anything.

So, reading this we get a clear indication that we need some sort of conditional statement here as our resultant type is dependent of whether any type in this array of type T is undefined or not.

But but, how are we going to check for each and every element in this T type array? Simple answer: we are not, we would just check if T extends an array of type R | undefined and if thats true, we would just return R[] type, else we need not do anything and simple return T as it is.

What is R here? Well, think of R as type variable. We can declare such variables using infer keyword. We would discuss the infer keyword in brief in later section.

Lets see the code first and then we are going to discuss it in more detail.

type RemoveUndefined<T extends any[]> = 
   T extends (infer R | undefined)[]
   ? R[]
   : T ;

So, in gist, the above snippet checks if T is a subset of (R | undefined)[] and returns R[] if true and T otherwise. Where R is a type variable which is a kind of placeholder during type definition but takes its actual value during type inference. Think of it a variable defined inside a function which takes value when function is executed, only this but for the type world. And we can declare such type variables using infer keyword.

But why did we say R | undefined why not simply R? Well, that is because saying R | undefined clearly separates undefined and leaves R holding the rest of the types EXCEPT undefined.

Lets try to understand this better with an example.

// Say, T is (string | undefined | number)[]

T extends (infer R | undefined)[]

(string | undefined | number)[] extends (infer R | undefined)[]

// (string | undefined | number) is equivalent to (string | number | undefined) right?

(string | number | undefined)[] extends (infer R | undefined)[]

// We can notice that if R takes the type (string | number),
// this statement would be interpreted as true

Please mind one thing that in above example, R can hold a lot of types other than (string | number), it can hold any, unknown, (string | number | boolean) etc, BUT (string | number) is the most conclusive/strict type it can take, hence this works for us.

Here, as we can notice, we successfully separated out undefined from rest of the types in T. And once we have all "types" other than undefined, we can return an array type of those "types". Therefore, we do not simply return R but an array of type R that is R[].

Great! That concludes our RemoveUndefined type, you can play with it here. This same type with a bit of modification is used in just-flush, as it was contributed by me only 😜

Please note that, if you pass a tuple type, the order wont be maintained.

BUT, we have one more thing to discuss, that is the infer keyword. So here is a...

Quick Aside on infer keyword

As discussed earlier, infer keyword in used to declare type variables. Referring back to what was previously mentioned:

Where R is a type variable which is a kind of placeholder during type definition but takes its actual value during type inference. Think of it a variable defined inside a function which takes value when function is executed, only this but for the type world. And we can declare such type variables using infer keyword.

So essentially, we use type variables for types that we during writing the type are not sure about, mostly it is something that is dependent on the incoming type param.

Lets look at an example, in this example, we would write a generic type that takes T as a type param, and using this type, we can conclusively get the type of elements T holds considering that T would always extend Array type.

type TypeOfArray<T extends any[]> = T extends (infer R)[] ? R : never;

Here, we do not know what kind of array would T extend, so we say, lets assume that T extends an array of type R. Now, this R is not known to us, and would be derived from the type param coming in that is T. And the infer keyword helps us declare such variables.

Hope you have a better idea about the infer keyword in TS. I have added another example for usage of infer, in this type, we are going to get the type of params the function receives, this function type would be passed as a type param.

type Params<T extends (...args: any[]) => any> = T extends (...args: infer R) => any ? R : T;

type Test = Params<(arg0: number, arg1: number, arg2: Date) => string> 
// type Test = [arg0: number, arg1: number, arg2: Date]

Conclusion

In this blog, we explored Conditional Types and the infer keyword in the world of TypeScript and using these, we created a generic type that removes undefined from an array type. Code for the same is available here. Everything mentioned here is as per my understanding, please do not take them to be 100% accurate. I am also dropping some relevant links to read further.

Find me at:-

Thank you, and have a great day!!