Published at
Updated at
Reading time
2min
This post is part of my Today I learned series in which I share all my web development learnings.

Is it just me, or are TypeScript conditional types and the extends keyword kinda scary?

ts
// What is going on? 😅
type Something<T> = T extends number ? never : T;

I've just read Dr. Axel's "Conditional types in TypeScript" and some things finally clicked for me.

Let's say we have a union type containing multiple strings.

ts
type AllColors = "Black" | "White" | "Orange" | "Red" | "Blue" | "Yellow" | "Gray";

Some people don't consider black, white and gray as colors. How could you now iterate over the union type and get these "noncolors" out of the AllColors type?

ts
// Iterate of the types included in `AllColors`.
//
// If type `T` does not extend `R` apply the `never` type.
// `never` will remove `T` from the resulting union type.
type Remove<T, R> = T extends R ? never : T;
 
// Remove "Black" and "White" from "AllColors" union type.
type RealColors = Remove<AllColors, "Black" | "White" | "Gray">;
type RealColors = "Orange" | "Red" | "Blue" | "Yellow"

Using conditional types, you can iterate over and filter union types. If you return never, it will be excluded from the resulting union type.

TypeScript includes the built-in utility types exclude and extract for these use cases, and I only picked this example to explain the concept.

That's pretty cool, but you can use conditional types also to iterate over a union type and map the resulting types.

In this example, the strings are prefixed with String:.

ts
type Random = "Joo" | "Foo" | 123 | 234;
 
// Iterate of the types included in `Random`.
//
// If type `T` is of type `string` prefix it.
type Prefix<T> = T extends string ? `String: ${T}` : T;
type MappedTypes = Prefix<Random>;
type MappedTypes = 123 | 234 | "String: Joo" | "String: Foo"

Or, if we take the color example, we could also iterate and append (noColor) to Black, White and Gray.

ts
type AllColors = "Black" | "White" | "Orange" | "Red" | "Blue" | "Yellow" | "Gray";
 
// Iterate of the types included in `AllColors`.
//
// If type `T` is of type `string` and `T` is of type `R`
// append parentheses to the string type.
type Suffix<T, R> =
T extends string
? T extends R ? `${T} (no color)` : T
: T;
type MappedColors = Suffix<AllColors, "Black" | "White" | "Gray">;
type MappedColors = "Orange" | "Red" | "Blue" | "Yellow" | "Black (no color)" | "White (no color)" | "Gray (no color)"

Granted, the nested ternary isn't pretty, but it seems to be the only way to include two conditions to TypeScript's conditional types.

And as a last trick: if your union type only includes strings, you can spare all this extends dance and use template literal types.

ts
type AllColors = "Black" | "White" | "Orange" | "Red" | "Blue" | "Yellow" | "Gray";
type AllPrefixedColors = `Color: ${AllColors}`;
type AllPrefixedColors = "Color: Black" | "Color: White" | "Color: Orange" | "Color: Red" | "Color: Blue" | "Color: Yellow" | "Color: Gray"

Good stuff!

If you enjoyed this article...

Join 5.7k readers and learn something new every week with Web Weekly.

Web Weekly — Your friendly Web Dev newsletter
Reply to this post and share your thoughts via good old email.
Stefan standing in the park in front of a green background

About Stefan Judis

Frontend nerd with over ten years of experience, freelance dev, "Today I Learned" blogger, conference speaker, and Open Source maintainer.

Related Topics

Related Articles