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 anone
string literal type if no border width is given.setBorder
returns aBorderStyle
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 theextends
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)
, whereT
is the return type.- If this function is assignable to function
() => string | number
, thenT
will be the resulting type, otherwisenever
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.