Advanced Conditional Types
🌲 Advanced Conditional Types
infer patterns, and how to build genuinely reusable type guards on top of all of it.
Quick Recap
A conditional type picks between two branches based on an extends check, evaluated entirely at compile time:
Nested Conditional Types
Conditional types chain the same way if/else if/else does — each : can itself be another conditional:
This is exactly how TypeScript's own built-in Parameters<T>, ReturnType<T>, and similar utility types are implemented under the hood — a chain of conditions, each narrowing what T could be.
🔀 Distributive Conditional Types
The single most surprising rule in this chapter: a conditional type distributes over a union when the checked type is a "naked" type parameter — it runs the check separately for each union member and unions the results back together:
Distributive vs Non-Distributive
Distributive (naked type parameter)
Non-Distributive (wrapped in a tuple)
This is exactly how Exclude<T, U> is implemented — it relies on distribution to filter a union member-by-member:
🔍 Deeper Inference Patterns
infer can appear more than once, and in more places than a simple function return type:
Recursive Conditional Types
Awaited2 calls itself in its own true-branch — the type checker recurses just like a function would, unwrapping nested Promise<Promise<T>> layers.
infer in a Tuple Position
infer A in (...args: infer A) captures the entire parameter list as one tuple type, not each parameter separately.
Complex Type Guards Built From Conditionals
Some checks that feel like they should be simple turn out to need real conditional-type trickery — the classic example is detecting any, which behaves unlike every other type:
IsAny<T>: Detecting the One Type That Breaks the Rules
This is an intentionally deep-cut example — most real code will never need IsAny<T> directly, but understanding why it works is a good test of whether the distributivity and inference rules above actually clicked.
💻 Coding Challenges
Challenge 1: Build a Nested Conditional
Write a Flatten<T> type that: returns T's array element type if T is an array, returns T's Promise-resolved type if T is a Promise, and returns T unchanged otherwise.
Goal: Practice chaining multiple extends checks in sequence, each with its own infer.
Challenge 2: Exploit Distribution
Write a type NonFunctionKeys<T> that, given an object type, produces a union of only the keys whose values are not functions.
Goal: Practice combining a mapped type with a distributive conditional to filter keys based on their value's type.
Challenge 3: Write DeepAwaited
Write a recursive conditional type DeepAwaited<T> that fully unwraps any level of nested Promise<Promise<...Promise<T>...>> down to the innermost non-Promise type.
Goal: Practice a conditional type that recurses on itself until a base case is reached.
Distribution happens automatically whenever the checked type is a bare, unwrapped type parameter — which means it can happen when you didn't want it, silently turning MyType<A | B> into Result<A> | Result<B> instead of one combined result. If a conditional type is behaving strangely on a union input, wrap both sides in a single-element tuple ([T] extends [U]) to force a non-distributive, whole-union comparison, then decide deliberately whether distribution is actually what you want.
🎯 What's Next
Conditional types can check a shape — the next chapter tackles shapes that reference themselves: Recursive Types & Tree Structures, representing trees and graphs at the type level, with depth-aware constraints to keep the compiler from recursing forever.