
Gentle Introduction to Typescript Conditional Types
Basics
Consider the following function.
1function toggle(input: number | string) {
2 switch(typeof input) {
3 case 'string':
4 return parseFloat(input);
5 case 'number':
6 return `${input}`;
7 }
8}
Calling toggle()
with a numeric argument will result in a string, and passing a string argument will result in a number.
What if we could create a Typescript (utility) type that does something similar?
1type Toggle<T> = T extends string ? number : string;
2
3//===> TESTS <===
4type test1 = Toggle<string>;
5// ^?type test1 = number
6type test2 = Toggle<number>;
7// ^?type test2 = string
8type test3 = Toggle<'happy'>;
9// ^?type test3 = number
10type test4 = Toggle<-123>;
11// ^?type test4 = string
From the tests, you can see that we can toggle from one type to another.
Aside
There is a problem with Toggle<>.
It will produce invalid (unexpected) results in specific scenarios. Can you see when these invalid results will occur?
Answer: It fails when any type but `string | number` is passed into the utility type.
1//---> Toggle Fails :-(
2type oops1 = Toggle<boolean>;
3// ^?type oops1 = string
4type oops2 = Toggle<{}>;
5// ^?type oops2 = string
6type oops3 = Toggle<[]>;
7// ^?type oops3 = string
8type oops4 = Toggle<null>;
9// ^?type oops4 = string
10type oops5 = Toggle<undefined>;
11// ^?type oops5 = string
Why do you think all of these examples return `string`?
A Better Toggle<>
We can improve Toggle<>
by simply adding a type guard to the generic
1type BetterToggle<T extends string | number> =
2 T extends string ? number : string;
3
Now, all of the oops tests above will result in errors as BetterToggle<>
only accepts string
or number
arguments!
Playground with both Toggle<> and BetterToggle<>
Complex Ternary
In the Toggle
the example above, we see that Typescript has the concept of a ternary operator when creating types.
Let's consider a situation where we need a type utility that will return one of two types based on some boolean condition.
If<C,T,F>
takes three arguments:
- C - the Boolean condition
- T - the type returned if C is true (truthy result)
- F - the type returned if C is not true (falsy result)
1type If<C,T,F> = C extends true ? T : F;
Let's look at some test cases...
1type _true = If<true, 'T', 'F'>
2// ^?type _true = "T"
3type _false = If<false, 'T', 'F'>
4// ^?type _false = "F"
5type _mixT = If<true | string, 'T', 'F'>
6// ^?type _mixT = "T"
7type _mixF = If<string | false, 'T', 'F'>
8// ^?type _mixF = "F"
The first two tests (_true and _false) should be easy to predict.
true
extends true
so 'T' is returned
false
does not extend true
so 'F' is returned
The last two tests (_mixT
and _mixF
) give us a better understanding of the extends
keyword.
extends
We have used extends
to have an interface inherit properties from another interface.
interface IChild extends IPerson { /*...*/ }
Earlier, we used extends
to add a constraint to the generic type Toggle
type BetterToggle<T extends string | number> = /*...*/
Here, extends
means that T must be a string or a number
Now, we are using extends to create the condition of a ternary statement in types.
type If<C,T,F> = C extends true ? T : F;
The condition C extends
true does not mean "is C equal to true." Instead,
extends
means "is a superset of" or "Is true in C?"
In our test cases, both true
and true | string
are supersets of true
(the both contain true
).
Likewise, false
and string | false
are not supersets of true
Here comes the complex part...
Here are a few tests that do not work as intended. Look at these and see if you can explain why they all result in 'F' even though the condition is not falsy.
1//What about these...
2type _zero = If<0, 'T', 'F'>
3// ^?type _zero = "F"
4type _one = If<1, 'T', 'F'>
5// ^?type _one = "F"
6type _obj = If<{}, 'T', 'F'>
7// ^?type _obj = "F"
8type _arr = If<[], 'T', 'F'>
9// ^?type _arr = "F"
(answer: any condition that does not extend true
will return the falsy value, even if the condition does not extend false
.)
We might be tempted to put a type guard (constraint) on the condition (e.g. C extends boolean
) but what we really want is for IF<>
to return never
, rather than result in an error.
1type If<C, T, F> =
2 C extends true
3 ? T
4 : C extends false
5 ? F
6 : never;
In this version of If<>
, we have a complex ternary statement. If C extends true
, we simply return the truthy result (T). But if it doesn't, we check to see if C extends false
. If it does, we return the falsy result (F); if it doesn't, we return never.
You can imagine that we can create multiple levels of ternary statements.
Check out this Playground to explore more with the If<>
utility including a Ifish<>
utility that handles falsy (e.g., 0, '', null, undefined).
Challenge
Create a utility type IsType<>
that accepts two type arguments and returns true if the first extends the second.
Extra Credit...
What would be the result of IsType<"hi" | 12, number>
? Why?