Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 a type: string and a payload: T.
  • Create a conditional type called ExtractPayload<T>.
  • If T is a WrappedEvent, ExtractPayload should return the type of the payload (use infer).
  • If T is not a WrappedEvent, it should just return T itself.
// 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 SystemAlert
Rating
0 0

There are no comments for now.

to be the first to leave a comment.