Tuesday, April 06, 2021

Conditional Types in Typescript

This post is about conditional types in typescript, which happens to be an interesting and powerful feature of the typescript's type system. It is part of the Introduction to Advance Types in TypeScript series.

Conditional types can be seen as a mechanism that allows us to perform ternary operations on types. 

Normally, ternary operations work with values, for example:

resulting value == true ? value A : value B
let value = 10
const isEven = value % 2 == 0 ? true : false
Conditional types gives us the ability to perform similar operation on types. Where we get a type out of two possibility, depending on the outcome of a check. 


When ternary operations are performed using values, the condition that determines which value is returned is is a boolean check, with conditional types, the operation that determines which type is returned is an assignability check.  It looks like this:
resulting type == true ? type A : type B
type AString = string
type Result =  AString extends string ? number : string

The assignability check is basically a question that asks if a type is assignable to another. This is done using the extend keyword.

The above example is trivial as we can see that AString is a string and hence will be assignable to string, passing the assignability check. A more realistic use case, usually involves generics, where the type to use for the assignability check and optionally the results are generic parameters. 

This look like this:
Conditional<T, U, V> = T extends Type: U : V
would be the type used for the assignability check, will be the type that would be returned if assignability check passes, otherwise type V will be returned. A real life example of conditional types, taking from Typescript's standard library is NonNullable. It's definition looks like this:
type NonNullable<T> = T extends null | undefined ? never : T;
The source can be seen here The NonNullable conditional type can be used to defined a function that can never be called with a value that can be null. If a value could be null, then compilation fails. For example this will compile fine:
function nullSafe<T>(value: NonNullable<T>) {
    console.log(value)
}

let value = 42;
nullSafe(value)
while this will not:
function nullSafe<T>(value: NonNullable<T>) {
    console.log(value)
}

var value;
nullSafe(value)
In this particular instance, apart from being a conditional type, NonNullable also makes use of the never type (as can be seen in the definition) to achieve this sort of type safety. never has already been covered in any, unknown and never types in Typescript

The general suggestion of seeing the advance type features of typescript as a mechanism the language provides for generating types based on other types apply to conditional types. This is because this is the essence of a conditional type: get a type, out of two types based on the result of an assignment check.

The other common use case of conditional type are as a replacement for function overloading in Typescript.

For example, given the following overloaded function:
function processStringOrNum(value: number): string
function processStringOrNum(value: string): number
function processStringOrNum(value: string | number): string | number { 

  if (typeof value === "string") { 
      return value.length 
  } else { 
      return value.toString() 
  }
}

which will return a value of string given a number as input. And given a number as input will return a value of type string.

If you think about it, this is exactly the sort of things we could use conditional type for. What we want to do is to have the return type of the function depends on the type of the argument. Expressing this using conditional types will look like this:
function processStringOrNum<T extends string|number>(value: T): Cond<T> {
    if (typeof value === "string") {
        return value.length as Cond<T>
    } else {
        return value.toString() as Cond<T>
    } 
}
These is just scratching the surface of what can be achieved using conditional types in Typescript. Hopefully this post provides a good understanding of the basics and motivation to explore further.


This post is part of a series. See Introduction to Advance Types in TypeScript for more posts in the series.

Are you a developer coming to typescript from other programming languages? Then you will find my upcoming book useful: TypeScript Beyond The Basics. Sign up here to be notified when it is ready.

No comments: