TypeScript: Conditional Types Explained

Learn what conditionals are and how they are used in TypeScript

Conditionals in TypeScript, also introduced in the TypeScript handbook, allow us to deterministically define types depending on what parameterised types consist of. The general basic rule is:

type ConditionalType = T extends U ? X : Y

If parameter T extends some type U, then assign X, otherwise assign Y.

The extends keyword is at the heart of conditionals whereby we are checking if every value of T can be assigned to a value of U. If T is assignable to U, then the “true type” will be returned — X is our case. If T is not assignable to U, then the false type, Y, will be returned.

As a basic example, consider checking whether some type extends a primitive such as a string, and assign never if it does not:

type StringOrNot = SomeType extends string ? string : never;

The never keyword indicates that a value will never occur — we will cover never in more detail further down when conditionals are used to coincide with type filtering.

Conditionals are often coupled with generic types (otherwise termed type parameters) to test whether such parameter meets a certain condition. With one generic type, StringOrNot could be expanded such that it will no longer be limited to just SomeType to test against string:

type StringOrNot<T> = T extends string ? string : never;

This piece will explore the following topics of conditional types :

  • Working with conditionals and union types and how they are used with common tasks in TypeScript.
  • The differences between checking over the distribution of a union type versus non-distributive types. The difference between the two methods will be explained.
  • Some of the TypeScript utility types will be explored, covering how they use conditional types to achieve their logic.

Let’s get started with union types and how they fit into the conditional type puzzle.

Conditionals and Union Types

Union types allow us to assign multiple valid types to a type name, thus satisfying a higher range of values. For example, a union type could be defined to cover a range of a particular set:

type AllSets = 'SET A' | 'SET B' | 'SET C'

These types are called string literal types, as the type itself is a string value. If you were working with a function that generates some border styling, the following union type consisting of string literals would cover all possible border styles:

type BorderStyles = 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset' | 'none' | 'hidden';function setBorder(val: BorderStyles): void {
...
}

Number literals can also be used. Let’s say we wish to define a border width type to cater for all supported border widths supported in our app:

type BorderWidths = 1 | 2 | 3 | 4 | 5;

What we’ll do in this section is conditionally type the setBorder function such that:

  • setBorder returns a none string literal type if no border width is given.
  • setBorder returns a BorderStyle type, consisting of the border width and style type, if a width is indeed given.

The two union types we’ve already defined can actually be combined into a more comprehensive BorderStyle union type. Examples for BorderStyle include 1px solid, or 3px dotted, etc. With a neat TypeScript feature, we can indeed create a union type that covers all combinations of BorderWidths and BorderStyles.

Concretely, as of TypeScript 4.1 we can combine union types to create a more fully-featured union type. This results in less syntax to maintain, and more modularity between your types. Such types are called template literal types, and they are very useful.

Consider the following type that will cover all possible values for our setBorder function by combining BorderStyles and BorderWidths:

type BorderStyle = `${BorderWidths}px ${BorderStyles}`;// Takes
// 1px solid | 1px dashed | ...
// 2px solid | 2px dashed | ...
// ... | ... | ...

BorderStyle can now be used as a part of setBorder’s return type, but as we mentioned, this return type could either be a BorderStyle or simply none. A conditional type is needed to cover both of these scenarios.

This is how that conditional type would be defined:

type Border<T extends BorderWidth | 'none'> = T extends BorderWidth ? BorderStyle : 'none';

Note that we are now turning our attention to generic type T that must either be assignable to a BorderWidth or a none string literal type. The conditional type then returns a true type of BorderStyle if T does indeed extend BorderWidth, otherwise it will return the none string literal.

Border<T> can then be plugged in as the return type of setBorder. Here is the fully-implemented function signature:

function setBorder<T extends BorderWidth | 'none'> (width: T, style: BorderStyles): Border<T> {
throw "unimplemented"
}

Note that T needs to extend BorderWidth and none both in the function signature and the type definition. Without this context, T could be any value and compile time errors will spring up as a result.

This example shows the power of simple conditional types in a real-world use case, with some neat TypeScript tricks with the template literal feature that was recently rolled out.

Now we have seen a simple use case of conditional types we will explore some more advanced concepts of them, starting with filtering types.

Filtering types with conditionals and never keyword

We used never at the beginning of this article to demonstrate the most basic conditional statement. If you were wondering how never can be practically used, you’re are now in the right place.

Consider the following two types:

type StringOrNumberOnly<T> = T extends string | number ? T : never;type MyResult = StringOrNumberOnly<string | number | boolean>;// MyResult = string | number

MyResult has been generated by checking whether the given union conforms to StringOrNumberOnly<T>. We are effectively defining MyResult by excluding types that do not return true in MyType. MyResult here simply discards every type resulting in the conditional statement’s “false type”, being never, effectively removing any chance of that type being a part of the MyResult union.

What we did above is define a simple exclusion type, but for such abstract types we can usually rely on the utility types included in the TypeScript standard library. Let’s take a look at a couple of the more common ones here.

Extract and Exclude utility types

Extract and Exclude are good examples of conditional type usage in TypeScript. Given a union type, they are able to filter that type according to the types you provide. Here is the definition of Extract:

type Extract<T, U> = T extends U ? T : never;

Given two parameters T and U, Extract will check if T can be assigned to U, and discards that type if it does not. Extract will always yield a subset of U.

To demonstrate Extract, consider the following use case where our ResultType will be MyUnion minus the string type:

// some union type
type MyUnion = string | boolean | never;
// ResultType will extract from `MyUnion`
type ResultType = Extract<MyUnion, boolean | string | object>;
// ResultType = string | boolean;

Notice that even though we supplied object for Extract’s U parameter, it does not exist in MyUnion and will therefore not be extracted and included in the ResultType union.

Exclude is similar in nature, but will remove the given types from a union if they exist. The definition of Exclude is the reverse of Extract:

type Exclude<T, U> = T extends U ? never : T;

Using MyUnion again, we can see how Exclude will work with a simple use case:

// some union type
type MyUnion = string | boolean | never;
// attempt to exclude some types from `MyUnion`
type ResultType2 = Exclude<MyUnion, string>;
// ResultType2 = boolean;

Extract and Exclude remove some boilerplate from your code, and are a part of a range of Utility types of TypeScript that are well worth getting familiar with. We’ll visit a few more of these further down.

Distributive vs non-distributive conditionals

Until now we have been discussing conditional statements with distributive types. In other words, the types of union T are being checked against all the types of the extends expression:

This is standard behaviour when T and the type T is being assigned to are standalone union types, but this breaks in a couple of conditions:

  • When T is a part of a larger expression such as a function, object or tuple. This type will be defined before the extends keyword.
  • When the type being checked against (the type after extends) is a function, object or tuple.

This becomes clear with examples, so let’s undergo a couple. Firstly we will define a function type that parameter T is a part of, where T will determine the return type of the function only:

type MyTypeFunction<T> = (() => T) extends () => string | number ? T : never;

Going through the syntax of this type:

  • T is now a part of the function (() => T), where T is the return type.
  • If this function is assignable to function () => string | number, then T will be the resulting type, otherwise never will be.

So what happens if we provide a union with additional types beyond string and number? The resulting type will be never:

Because T is no longer a standalone union type to be compared to (we are now comparing 2 function signatures), T must be assignable to string | number, as per the expected return type, otherwise it returns never.

Removing boolean from T or having just string or number will yield the true type of the conditional:

We would also get the same behaviour if tuples or arrays were being conditioned:

This is indeed a subtle difference and will become obvious as you become accustomed to writing more complex types, but it is nevertheless an important factor to consider when working with union types as a part of a more complex type.

With this understanding, the last section of this article will delve into some of the more advanced utility types TypeScript comes bundled with.

Utility Types and their Conditionals

Utility types commonly use conditional statements (along with other TypeScript keywords) to derive their resulting types. This section will briefly look at how some of these utilities have been defined to give the reader more intuition into how conditionals can be used.

The TypeScript documentation has many examples of using these types. Be sure to check them out if a type sparks your interest.

ReturnType<T>

The ReturnType<T> type simply returns a return type of a function. Here is its definition:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

For T to be valid, it has to be assignable to a function with any arguments and any return type — thats a very generic function that covers every signature possible!

What is interesting here is that the infer keyword is used to reference the function return type as R, and then a conditional is defined with R to either have it as the function return type, or any otherwise.

The infer keyword can be used with objects as well as functions, and will be explored in more detail in another piece that will be linked here once published.

Although not defined in the TypeScript utility types, we can take the same approach to extracting a return type to extract a resulting promise type (the type of the resolved value of a promise). This can be achieved with the basic Promise type:

type PromiseType<T> = T extends Promise<infer U> ? U : never;

infer has been used here to extract the Promise type rather than a function return type, but the same idea applies.

Omit<Type, Keys>

Omit builds upon the concepts of Exclude and Extract. It firstly takes all the types from its Type parameter, that is commonly an interface for an object, and then removes only the Keys provided:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Both Pick and Exclude utility types have been used to make Omit as elegantly defined as the above definition. Pick<Type, Keys> will construct a type consisting of a number of properties Keys from a Type. Just for good measure, let’s check out how Pick is defined too:

type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

The in keyword can be thought of as a loop (think of an ordinary for-loop) whereby we are looping through all properties P in K and extracting their types.

To understand the keyof and in keywords in more detail, I have published another piece that delves more into these key features: TypeScript: Typing Dynamic Objects and Subsets with Generics.

Bonus: Required<T>

Required is not strictly a conditional type, as it simply marks all properties of a generic type required. However, the syntax may throw you off upon first inspection. The following is the definition of Required<T>:

type Required<T> = {
[P in keyof T]-?: T[P];
};

The -? syntax removes the optional property modifier (?) from all the keys of T. This should not be confused with a condition.

The opposite of Required<T> is Partial<T>, where all properties will be set to optional.

In Summary

This article has introduced how conditional types can be used in TypeScript. We started by exploring union types and how they are well suited to working with conditional types. We can test whether a subset of types exist in some union type, or exclude certain types from a union.

Functions that have arbitrary return types were discussed. The setBorder example illustrated how return types can be dynamically assigned with conditional types.

The difference between distributive and non-distributive types was explored, whereby generic types that end up being a part of a wider type definition (not standalone) will no longer be distributive. This ultimately affects how the extends keyword checks whether a type is assignable to another — these differences become second nature with practice.

The article finally looked at some of the common utility types bundled within TypeScript standard library, and how conditionals have been used to achieve their definitions, along with other of TypeScript.

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store