Gentle Introduction to Typescript Conditional Types
typescript-today

Gentle Introduction to Typescript Conditional Types

Using `extends` as a ternary expressions when typing

Basics

Consider the following function.

typescript
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?

typescript
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.

typescript
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

typescript
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)
typescript
1type If<C,T,F> = C extends true ? T : F;

Let's look at some test cases...

typescript
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.

typescript
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.

typescript
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.

Solution

Extra Credit...

What would be the result of IsType<"hi" | 12, number>? Why?

Answer

popularity
Electric
posted
Mar. 17, 2023