TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
30: Conditional Types
Imagine you're standing at an airport security checkpoint. The agent looks at your documents and makes a decision based on what you're holding. If you have a diplomatic passport, you're ushered straight to the VIP lounge. If you have a standard passport, you go to the regular queue. If you have no passport at all, you're sent back to the check-in counter. The "result" (where you end up) depends entirely on the "type" of document you present.
In TypeScript, conditional types do exactly this. They allow us to say: "If type T is assignable to type U, then result in type X, otherwise result in type Y." It's essentially a ternary operator, but instead of operating on values at runtime, it operates on types during compilation.
The Type-Level Ternary
The syntax looks familiar if you've spent any time in JavaScript. It follows the T extends U ? X : Y pattern. I like to think of the extends keyword here as meaning "is a kind of" or "fits into."
type IsString<T> = T extends string ? "It is a string" : "It is something else";
type A = IsString<string>; // Type is "It is a string"
type B = IsString<number>; // Type is "It is something else"
Now, you might be thinking, "Why would I ever need this?" In a small project, you probably wouldn't. But once you start building complex libraries or generic API wrappers, you'll hit a wall where the return type of a function depends entirely on the input type. That's where this becomes a superpower.
Extracting Types with the Infer Keyword
This is where things get interesting. Sometimes you don't just want to check if a type fits; you want to reach inside that type and grab something out of it. TypeScript gives us the infer keyword for this. It's like telling the compiler, "If this fits the pattern, call the part I'm interested in 'U' and let me use that."
Let's look at a real-world scenario: handling API responses that might return a single object or an array of those objects. You want a utility that always gives you the type of a single item, regardless of how it arrived.
type UnwrapArray<T> = T extends Array<infer Element> ? Element : T;
interface User {
id: number;
name: string;
}
type SingleUser = UnwrapArray<User>; // Result: User
type FromList = UnwrapArray<User[]>; // Result: User (it inferred the element!)
I've used this pattern countless times when building data-fetching hooks. It saves you from having to manually define "Single" and "List" versions of every single model in your codebase.
The Magic of Distributive Conditionals
There is one "gotcha" that often trips people up: when you pass a union type into a conditional type, TypeScript doesn't treat the union as a single unit. Instead, it "distributes" the check across every member of the union.
If you pass string | number into a conditional type, TypeScript effectively runs the check for string, then runs it for number, and joins the results back together into a union.
type ToArray<T> = T extends any ? T[] : never;
type StringOrNumberArray = ToArray<string | number>;
// Result is string[] | number[], NOT (string | number)[]
If you actually want the latter, you can wrap the types in square brackets [T] to disable this distribution. It's a subtle detail, but it's the difference between a bug that takes two hours to find and a perfectly typed system.
📋 Practical Task
Build a Dynamic Event Payload Extractor
You are working on an event-driven system. Some events are "Wrapped" (they contain metadata and a payload), while others are "Raw" (the event itself is the data). You need to create a type utility that extracts the actual data payload regardless of whether the event is wrapped or raw.
Requirements:
- Define a
WrappedEvent<T>interface that has atype: stringand apayload: T. - Create a conditional type called
ExtractPayload<T>. - If
Tis aWrappedEvent,ExtractPayloadshould return the type of thepayload(useinfer). - If
Tis not aWrappedEvent, it should just returnTitself.
// Test your implementation with these:
interface UserLoggedIn { userId: string }
interface SystemAlert { code: number }
type EventA = WrappedEvent<UserLoggedIn>;
type EventB = SystemAlert;
type PayloadA = ExtractPayload<EventA>; // Should be UserLoggedIn
type PayloadB = ExtractPayload<EventB>; // Should be SystemAlertThere are no comments for now.