How to better understand TS generics? Conditional Types

Matan Cohen
Wix Engineering
Published in
6 min readDec 6, 2022
Photo by Alexander Grey on Unsplash

This is my second blog post in the TypeScript series. The link to the first part is below:

In this article we will cover a more advanced feature — Conditional Types.

Conditional Types

Sometimes we want to ask questions about our types and for that purpose we can use Conditional Types.

When using Conditional Types, you do not have to necessarily use Typescript Generics, but they are very powerful together.

Before we see some use cases of Conditional Types and Generics, let’s understand how to use Conditional Types.

Conditional Types have the same syntax as the Ternary Operator in JS,

for JS value we use

condition ? exprIfTrue : exprIfFalse

for example:

const lunch = isVeryHungry ? 'Burger' : 'Salad';

Basically, shorthand for if-else that always resolves to a value.

And on types:

We can see the Conditional Types DogOrCat , in lines 11–13 we can see the usage.

If we provide the type value of 'dog' we get back a type of Dog . Otherwise, we will get back Cat type.

Conditional Types has this syntax:

SomeType extends OtherType ? TypeIfTrue : TypeIfFalse;

Let’s break it down:

  1. SomeType extends OtherType : condition, extends checks that someType at least as specific, if not more specific, than OtherType .
  2. TypeIfTrue — expression type if the condition is true.
  3. TypeIfFalse — expression type if the condition is false.

To make sure we understand how Conditional Types works let’s go over two more examples:

What is the type of result1 and result2 ?

Think about it for a second before you scroll down!

.

.

.

.

.

.

Drumroll please, Dum Dum Dum, the answers are:

type result1 = true
type result2 = false

Don’t worry if you got it wrong, it’s a little bit confusing in the beginning.

A helpful way to think about it is to think that all the possible groups of types on the lefthand side of extends are contained in the group of type on its righthand side.

The group on the right is the only 's' one that is contained in the group of all possible strings, so type result1 = true .

the group of all strings is not contained in the group that has only 's' so type result2 = false

Now that we know how to use conditionals, let’s see why they are useful when combined with Typescript generics.

Extract and Exclude:

Extract and Exclude types are used to obtain a subtype from a primary type.

What Extract does?

Extract<Type,Union>

Extract is getting from the generic Type all the union members that are assignable to Union .

Not so clear? So, let’s use an example:

Extract will “take” all the types from Width that match string or { type: string } .

Which means that:

type KeyWordWidth = 'auto' | 'min-content' | 'max-content' | { type: string }

Exclude is exactly the opposite, removing whatever matches the Union.

Build time

Now that we understand what Extract and Exclude do, let’s build them ourselves using conditional types.

First, we will need to accept two type parameters T and U

type Extract<T,U>

Then we need to understand how extends works on union type.

When using extends on a union type, it does the same thing as usual just for each part of the union.

"a" | 5 extends string | number

It’s the same as:

"a" extends string OR
"a" extends number OR
5 extends string OR
5 extends number

When using never type in a union with another type, the never will disappear. For example, number | never is just number

Putting it all together and we got:

when using Type extends Union, the extends does all the magic for us, checking every combination:

Type = T1 | T2 | T3 ...
Union = U1 | U2 | U3 ...
T1 extends U1 ? 
T1 extends U2 ?
T1 extends U3 ?
T2 extends U1
...

In Extract if the condition is true, we add the type to the result. But when using Exclude we “remove it” by using never.

Conditionals and generics have a lot more to offer and an article about them is not complete without talking about inference.

Inference with conditional types

The infer keyword can only be used in the “condition” expression of the conditional type.

We will use an example to understand why we even need the infer keyword.

We are using a third-party library in order to create an interactive graph.

This is a mock of the third-party function:

This function receives an option object, but the library does not expose the options in a separate type (oh no!).

An example of using this function:

Unfortunately, we are not getting any Errors even though we have misspelled width as wdth.

If the third party had exposed a type of options this could have been avoided. Luckily, we can infer the type ourselves.

To do that we will create a generic type with F type parameter, (F stands for function):

Then we will have to check that F is a function using conditionals:

It checks if F is any function, but we want a more specific type, a function with at least one argument.

And now for the typescript magic, we need to deduce the type of the first argument. For this we are going to use the infer keyword.

The infer keyword gives us a way to declare a new generic type in the condition type.

Here, instead of giving arg a concrete type we used the infer keyword to declare the new generic type A which represents the type of arg .

After we declare the new generic type there is only one place we can reuse it, the true branch of the conditional expression.

In our case if the check fails, then F is not a function that receives at least one argument. So type A could not exist.

But if the condition is true, A does exist and we can simply return it.

But what should we do if the condition is false? there are a few options:

  • We could use F and return the original type if there is no match, but that could be confusing.
  • We could use any but that is also misleading. Maybe we did match, and the function had a first argument of any, but in both branches, we get the same result.
  • If we want to know for sure that we did not match and something went wrong, we can use never.

never also disappears when used in union types so we could have something like this:

type Input = string | number | FirstFunctionArg<{}>

The FirstFunctionArg<{}> is never and disappears from Input type.

It’s time to put it all together, our Generic type:

The usage:

We used FirstFunctionArg to get the type of the first argument options and we are finally getting a TypeScript error on wdth , even a very useful one that tells us:

Did you mean to write 'width'?

Sum up

In this blog post we discussed Conditional Types which give us the ability to ask questions about our types.

We used conditional types to create ourselves the very useful Extract and Exclude generic types.

Using infer we were able to create a safer use for a third-party code.

Conditional types and Generics are great tools to make our code safer.

Use them wisely!

Sources

  • TypeScript Docs — Generics.
  • TypeScript Docs — Conditional Types.
  • Frontend Masters Course.

--

--

Matan Cohen
Wix Engineering

Frontend Tech lead at Wix, on the path of mastering functional programming.