TypeScript advanced types · 1 / 8
lesson 1

Conditional types

T extends U ? X : Y. A ternary, but for types — and it does something surprising with unions.

~ 18 min read·lesson 1 of 8
0 / 8

You're writing a helper that should accept a value or null and return the same type without null. The type system already has a name for it — NonNullable<T> — and reading its definition is the easiest door into the rest of this course. It looks like a ternary expression. It almost is one. But it has a quirk that makes it more powerful than the ternary you know.

The shape of a conditional

A conditional type uses the same ?: shape JavaScript uses for values, but it lives entirely in the type world. The expression on the left of ? is always a question of the form does type A fit into type B?

conditional.ts
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<"hello">;   // "yes"
type B = IsString<42>;        // "no"
type C = IsString<boolean>;   // "no"

IsString is a generic alias — a type that takes another type as input. The T extends string ? "yes" : "no" part runs at type-check time. TypeScript checks whether T can be assigned to string, then resolves to "yes" or "no" based on that answer. There is no code at runtime; the result is just a type.

The two <> around T make this a generic — a type that's parameterized. When you write IsString<"hello">, TypeScript fills in T with "hello", evaluates the conditional, and hands back the result.

What extends really asks

extends is the most overloaded word in TypeScript. In a class declaration it means inherits from. In a generic constraint (<T extends string>) it means must be assignable to. In a conditional type it means the same thing as the constraint — is the left side assignable to the right?

That single phrasing is the whole game. "hello" extends string is true because the literal "hello" is one specific value that fits in the string set. string extends "hello" is false because most strings are not literally "hello".

Tip

Read extends in conditional types as "is a kind of", not "inherits from". "red" extends string reads "red" is a kind of string — true.

A practical example: NonNullable<T> strips null and undefined from a type. The TypeScript lib defines it like this.

non-nullable.ts
type NonNullable<T> = T extends null | undefined ? never : T;

type X = NonNullable<string | null>;       // string
type Y = NonNullable<number | undefined>;  // number

Read it as a sentence: if T is null or undefined, give back never (the empty type, which disappears in a union). Otherwise, hand T back unchanged. But there's a subtlety in how it works on string | null that needs the next section to explain.

check your understanding
What does type R = "blue" extends string ? 1 : 2; resolve to?

Distribution over unions

Here's the move that makes conditional types feel different from a JavaScript ternary. When the type you check (the naked type parameter on the left of extends) is a union, the conditional doesn't ask the question once. It splits the union, asks the question for each member, and unions the answers back together.

distribute.ts
type ToArray<T> = T extends any ? T[] : never;

type R = ToArray<string | number>;
// step 1: split the union into string and number
// step 2: evaluate ToArray<string>  ->  string[]
// step 3: evaluate ToArray<number>  ->  number[]
// step 4: union the results          ->  string[] | number[]

Without distribution, you'd expect ToArray<string | number> to be (string | number)[] — a single array that holds either kind. With distribution, you get string[] | number[] — either an array of strings or an array of numbers, never mixed. This is rarely what you want for ToArray, but it's exactly what you want for filters.

NonNullable<string | null> works the same way. The conditional asks the question twice — once for string, once for null. string becomes string. null becomes never. The union of string | never collapses to just string. That's how the null gets removed.

NonNullable<string | null>string nullstring neverstring
A conditional type splits a union, evaluates each branch, then unions the results back together.

You can opt out of distribution by wrapping both sides of extends in a one-tuple. The brackets make the parameter no longer naked, and TypeScript treats the union as a single thing.

opt-out.ts
type ToArrayWhole<T> = [T] extends [any] ? T[] : never;

type R = ToArrayWhole<string | number>;
// (string | number)[]  — distribution turned off

The [T] and [any] wrap each side as a tuple of length one. They aren't doing anything semantically except blocking the distribution behavior. Library authors reach for this when they want a conditional type to reason about the union as a whole.

check your understanding
What does type R = (1 | 2 | 3) extends number ? "y" : "n"; resolve to?

The inheritance trap

The keyword extends carries a heavy load from class-based languages, and that mental baggage causes real bugs. Here's a snippet that looks reasonable until you trace it.

broken.ts
// Trying to check "is T a subtype of Animal?"
class Animal {}
class Dog extends Animal { bark() {} }

type IsAnimal<T> = T extends Animal ? true : false;

type A = IsAnimal<Dog>;     // true   (a Dog is an Animal)
type B = IsAnimal<{}>;      // true   (!) — an empty object also passes

The second line surprises people. {} reads as the empty object type, but in TypeScript it actually means anything that isn't null or undefined. So an empty object passes the is assignable to Animal check, even though no programmer would call {} an animal.

The lesson is to stop reading extends as inheritance and read it as fits in. The check is structural — TypeScript looks at the shape of the type, not at any class hierarchy. If your conditional needs to be tighter than that, you have to add more constraints (like checking that a specific method exists).

Watch out

In TypeScript, {} is the loosest non-null type, not a literal empty object. Anything that isn't null or undefined extends it. Use Record<string, never> when you mean truly empty.

Try it yourself

check your understanding
You write type R = string extends "hi" ? 1 : 2;. What's R?
check your understanding
What is type R = NonNullable<null | undefined>;?
check your understanding
You want a conditional type that treats string | number as a single thing instead of distributing. Which form does that?
next lesson →
KeepLearningcertificate
for completing
TypeScript advanced types
0 of 8 read