How to better understand TS generics? Conditional Types
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:
SomeType extends OtherType
: condition,extends
checks thatsomeType
at least as specific, if not more specific, thanOtherType
.TypeIfTrue
— expression type if the condition is true.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 ofany
, 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.