What are conditional types in Typescript?
Lets explore one of the most powerful yet confusing features of Typescript, conditional types. What are they? how do you use them?

Conditional typing is perhaps a weird idiom to grasp just by looking it at. Which is why, from my experience, many typescript developers work with it at just a base level,
since investing time into understanding how to write "proper" type-safe functions is too much of a hassle.
I personally think that if you decide to pick up typescript you have to learn it much differently than you would most other programming languages, instead of trying to just understand its syntax, you have to think about it as if you are the person dictating the syntax.
Simple answer
Conditional types are a way to create types that depend on other types.
They are a way to create a type that is based on a condition, and can be used to create more type-safe functions.
It is a way to determine types in an if-else manner.
From the documentation
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;Pretty straight forward, using what is basically a Ternary Operator you can ask the type system to check the type in your condition against another, and depending on the result return another type.
The extends keyword is basically a truthfulness operator here, in simple terms it asks:
Does Dog extend Animal? or is Dog a type of Animal?
- If it does, the typeof
Example1is a number - Otherwise, its a string
Based on the basics of polymorphism, we know that since the Dog interface does in-fact extend Animal, it is an interface that is also a type of Animal, which is whytype Example1 = number.
But what is it good for?
Your mind might be racing to find a use case for such a thing, it might not seem obvious at first, because this is the type system we are talking about.
We cannot use this for our variables, we cannot use this with JSON data.
But let me remind you of my previous statement, with typescript, we are dictating syntax, not just using it.
A use case
Say you want to implement a single function that takes either an array or an object and allows you to map over the collection.
An object is not an iterator, but any reader will be lying if they say that they never had a case where they needed to iterate over keys and values of an object.
The most obvious solution might be to just
const collection = {"Hello": "World", "foo": 123};
Object.entries(collection).map(([key, value]) => {
console.log("Key", key)
console.log("Value", value)
})While it helps plenty, I would like to have the type of the key be inferred, and not just a string.
While it's not a big deal, it is a good practice to have your types as specific as possible.
So why not create a utility function that iterates and manipulates both Objects and Arrays and gives us proper type hinting? Allowing us to basically map over our collections?
A Javascript implementation
Let start by implementing a version of our function, just as a proof of concept. We will implement the function in Javascript, and then convert it to Typescript.
function map(collection, callback) {
// If the collection is an array, map over it
if (Array.isArray(collection)) {
return collection.map(callback);
}
// If the collection is an object, map over its entries, and create a new object from them
return Object.fromEntries(
Object.entries(collection).map(
([key, value]) => [key, callback(value, key, collection)]
)
);
}
const arrMapRes = map([1,2,3, 'bla'], (value, key) => {
if (value == 1) return Math.random();
return "Hello";
});
console.log(arrMapRes);
// [0.02386789139876777, "Hello", "Hello", "Hello"]
const objMapRes = map({something: false, more: 'stuff'}, (value, key) => {
if (value == false) return false;
return Math.random();
});
console.log(objMapRes);
// {something: false, more: 0.02386789139876777}Now that we know how our Javascript implementation looks like, its a matter of converting it to Typescript. First we need to think about what we want to achieve.
- We want to be able to iterate over either an
Arrayor anObject - We want the correct types for our
keysandvaluesin the callback function - We want the result type of the function to be based on the return type of our callback
Typescript implementation.
Enter generics and conditional types.
We will begin by defining the type of our callback function based on our data type.
// Array map callback
type ArrayMapSignature<C extends Array<unknown>, U> = (
value: C[number],
key: number,
collection: C
) => U;
// Object map callback
type ObjectEntriesSignature<C extends Record<string, unknown>, U> = (
value: C[keyof C],
key: keyof C,
collection: C
) => U;There are multiple typescript tricks being used here so let me explain them.
C[number]is a way to get the type of the array values
- since
Cis an array, it is indexed by numbers C[number]is the type of the values in the array.
- since
C[keyof C]is a way to get the type of the object values
- since
Cis an object, it's values are indexed by predefined keys C[keyof C]is the type of the values in the object.
- since
C extends DataTypeis a way to ensure that the typeCinherits its type from theDataType.
We use another generic here, U is the return type of our callback function.
Conditional types
For our next type we are actually going to use conditional types, which will give us type hinting on the return type of our function.
// Conditional return type
type MapperReturnType<C, U> = C extends Array<unknown> ? U[] : Record<string, U>;Note that we are using the same extends keyword that we use for our generic types, where we instruct typescript to only allow types that are either an Array or an Object.
Here, using the same keyword, we are asking typescript to check if the type C is an Array.
if not, we assume it is an Object, since in our function we only allow these two types.
We again use another generic, U is the return type of our callback function.
The function
Now that we have our types, we can implement our function.
// Array map callback
type ArrayMapSignature<C extends Array<unknown>, U> = (
value: C[number],
key: number,
collection: C
) => U;
// Object map callback
type ObjectEntriesSignature<C extends Record<string, unknown>, U> = (
value: C[keyof C],
key: keyof C,
collection: C
) => U;
// Conditional return type
type MapperReturnType<C, U> = C extends Array<unknown> ? U[] : Record<string, U>;
/**
* Map over an array or object
* @param collection The collection to map over
* @param callback The callback function
* @returns The mapped collection
*/
function map<C extends Array<unknown> | Record<string, unknown>, U> (
collection: C,
callback: C extends Array<unknown> ? ArrayMapSignature<C, U> : C extends Record<string, unknown> ? ObjectEntriesSignature<C, U> : never
): MapperReturnType<C, U> {
if (Array.isArray(collection)) {
return collection.map(
callback as ArrayMapSignature<unknown[], U>
) as MapperReturnType<C, U>;
}
return Object.fromEntries(
Object.entries(collection).map(([key, value]) =>
[
key,
(callback as ObjectEntriesSignature<Record<string, unknown>, U>)(
value,
key,
collection
)
]
)
) as MapperReturnType<C, U>;
}
const arrMapRes = map([1,2,3, 'bla'], (value, key) => {
if (value == 1) return Math.random();
return "Hello";
});
console.log(arrMapRes);
// [0.2218412506876788,"Hello","Hello","Hello"]
const objMapRes = map({something: false, more: 'stuff'}, (value, key) => {
if (value == false) return false;
return Math.random();
});
console.log(objMapRes);
// {"something": false, "more": 0.0894643929759662}At first glance, this function may seem a bit overwhelming, but let me break it down for you.
- We define our function
mapwith two generics,CandU. - We state that whatever is passed as
collectionmust be either anArrayor anObject. - We define
Uand pass it toArrayMapSignatureandObjectEntriesSignature, as the return type for our callbacks.
On line 26 we see our first nested conditional type, which is a bit more advanced.
But why do we need to nest our conditional types?
After we test if C is an Array, C is assumed an Array (because of the extends keyword)
if it is not, typescript will only remember that C was assumed as an Array even though the assumption was wrong.
In that case we need to provide typescript with an alternative, which is why we make another assumption (extends) in the else clause of the conditional type,
think about this like an else if statement.
Our last else statement is the never type, which is a way to tell typescript that this should never happen, and if it does, throw an error.
Basically, we use the conditional type to check if the callback function should be an ArrayMapSignature or an ObjectEntriesSignature
Type casting
Why do need to still type cast our callback function?
While its not a good practice to type cast without a good reason, in this case we have a good reason, and we do it carefully.
Here, for example:
if (Array.isArray(collection)) {
return collection.map(
callback as ArrayMapSignature<unknown[], U>
) as MapperReturnType<C, U>;
}Since we are now deciding what to do with our function using values, typescript does not help us here.
We need to instead check the type of the collection parameter, which does not help typescript infer the type of the callback function.
But since we know that the type of the collection is an Array, and that our callback signature in this case will have to be ArrayMapSignature, we can safely type cast it.
And again with our MapperReturnType<C, U> conversion, since the Array.map function returns a U[], we can safely type cast it to our MapperReturnType,
the type of which will also be inferred by typescript as U[] since our collection in this case is an array.
Conclusion
This is an advanced example of how you can use conditional types in typescript, and while it may seem a bit overwhelming at first, it is a very powerful tool to have in your toolbox.
Hopefully this article has given you a better understanding of how to use conditional types in typescript, and how you can use them to create more type-safe functions.

Today I'm writing and architecting software using many different technologies, and I'm always looking for the next thing to learn.