Back to Blog
Lesson 41 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 28, 20264 min read

Conditional Types in TypeScript: Dynamic Logic for Robust Apps

Learn how to use Conditional Types to implement type-level branches. Master the ternary operator in TypeScript to build flexible, production-grade utilities.

TypeScriptAdvancedLogicGenericsAPI
Close-up of PHP code on a monitor, highlighting development and programming concepts.

Previously in this course, we explored Mapped Types in TypeScript: Dynamic Type Transformation, which allowed us to transform object shapes systematically. Now, we’re taking that a step further by adding decision-making capabilities to our type system using Conditional Types.

At their core, conditional types allow your types to "choose" a result based on a condition—much like an if-else statement, but for your type definitions.

Understanding Conditional Types from First Principles

In JavaScript, you’ve likely used the ternary operator (condition ? trueVal : falseVal) hundreds of times. TypeScript applies this same logic to types. A conditional type looks like this:

T extends U ? X : Y

This translates to: "If type T is assignable to type U, then the result is X; otherwise, it is Y."

This is the foundation of advanced TypeScript development, as it allows us to branch our logic based on the types we receive. We aren't just defining static structures anymore; we are defining rules for how those structures should evolve.

Implementing Type Branches

Let's look at a practical example. Imagine our task API client needs a utility that determines the return type of a function based on whether an "include metadata" flag is set.

TYPESCRIPT
type ApiResponse<T, IncludeMeta extends boolean> = IncludeMeta extends true
  ? { data: T; meta: { timestamp: number } }
  : { data: T };

// Usage
type SimpleResponse = ApiResponse<string, false>; 
// Result: { data: string }

type DetailedResponse = ApiResponse<string, true>; 
// Result: { data: string; meta: { timestamp: number } }

By using IncludeMeta extends boolean, we create a branch. If true is passed, the type includes the metadata object; if false, it doesn't. This pattern is exceptionally powerful when building reusable API wrappers, as seen in our work with Union Types for Flexibility in TypeScript.

Building a Flexible API Utility

In our ongoing Task API client project, we often need to handle different resource types. Sometimes we fetch a single task, and sometimes we fetch an array. We can use a conditional type to create a ResourceResponse utility that handles both scenarios automatically.

TYPESCRIPT
type Resource<T> = T extends any[] ? { items: T; total: number } : { item: T };

interface Task { id: string; title: string; }

// If we pass an array, we get an CE9178">'items' wrapper
type ListResponse = Resource<Task[]>; 
// { items: Task[]; total: number }

// If we pass a single object, we get an CE9178">'item' wrapper
type SingleResponse = Resource<Task>; 
// { item: Task }

This utility ensures that your API client code remains "DRY" (Don't Repeat Yourself). Instead of writing separate interfaces for list endpoints and single-item endpoints, you let the type system derive the correct structure for you.

Hands-on Exercise

To practice, create a StatusHandler<T> type. It should take a generic T. If T is exactly the string "pending", it should return { status: "waiting" }. If T is anything else, it should return { status: "processed" }.

  1. Define the type StatusHandler<T>.
  2. Test it with StatusHandler<"pending"> and StatusHandler<"completed">.
  3. Verify your results by hovering over the types in your IDE.

Common Pitfalls

  1. Over-complicating logic: Just because you can chain multiple ternary operators (e.g., A ? B : C ? D : E) doesn't mean you should. It quickly becomes unreadable. If you find yourself nesting more than two layers, consider breaking the logic into smaller, named types.
  2. Forgetting "any": When using T extends any[], remember that any can match almost anything in certain contexts. Be specific with your constraints if you run into unexpected behavior.
  3. Ignoring Distributivity: When you use a conditional type with a union (like string | number), TypeScript will apply the condition to each member of the union individually. This is often helpful, but it can lead to surprising results if you aren't expecting it.

FAQ

Can I use conditional types inside interfaces? Yes. You can use them for property types, which is a great way to make your interfaces dynamic.

Is there a way to perform an "else if" in conditional types? TypeScript doesn't have a specific else if keyword. You achieve this by nesting the ternary operator in the "false" branch: A ? B : (C ? D : E).

Are conditional types slow for the compiler? In large-scale applications with thousands of complex conditional types, you might notice a slight hit to IDE performance. Keep your logic simple to minimize this.

Recap

We've moved beyond static shapes to dynamic logic. By using T extends U ? X : Y, you can now:

  • Create branches in your type definitions.
  • Build flexible utilities that adapt based on generic inputs.
  • Reduce boilerplate by deriving response structures automatically.

These tools are essential for mastering the advanced patterns discussed in Mastering TypeScript Conditional Types: A Guide to Dynamic Transformation.

Up next: Inferring Types in Conditionals.

Similar Posts