Published at
Updated at
Reading time
2min

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 6.3k readers and learn something new every week with Web Weekly.

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