+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 64 of 72

๐ŸŽฏ Conditional Types: Type-Level Programming Magic

Master TypeScript's conditional types to write intelligent, adaptive types that make decisions at compile-time ๐Ÿš€

๐Ÿš€Intermediate
35 min read

Prerequisites

  • Understanding of TypeScript generics ๐Ÿ“
  • Basic knowledge of union types โšก
  • Familiarity with utility types ๐Ÿ’ป

What you'll learn

  • Master conditional type syntax and logic ๐ŸŽฏ
  • Create intelligent, adaptive types ๐Ÿ—๏ธ
  • Build custom utility types ๐Ÿ”ง
  • Debug complex type-level logic ๐Ÿ›

๐ŸŽฏ Introduction

Welcome to the fascinating world of conditional types! ๐ŸŽ‰ Think of conditional types as the โ€œif-elseโ€ statements of TypeScriptโ€™s type system - they let your types make smart decisions based on other types.

Youโ€™re about to unlock one of TypeScriptโ€™s most powerful features. Whether youโ€™re building libraries ๐Ÿ“š, creating type-safe APIs ๐ŸŒ, or just want to level up your TypeScript skills ๐Ÿš€, conditional types will transform how you think about types.

By the end of this tutorial, youโ€™ll be writing types that adapt, decide, and transform like magic! โœจ Letโ€™s dive into this incredible journey! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Conditional Types

๐Ÿค” What are Conditional Types?

Conditional types are like smart filters ๐ŸŽจ that examine a type and choose different outcomes based on what they find. Think of them as TypeScriptโ€™s crystal ball ๐Ÿ”ฎ - they look at types and predict what should happen next!

In simple terms: โ€œIf this type matches that pattern, then give me this result, otherwise give me that resultโ€

// ๐ŸŽฏ Basic conditional type syntax
type MyConditional<T> = T extends string ? "It's a string! ๐Ÿ“" : "Not a string ๐Ÿคทโ€โ™€๏ธ";

// ๐Ÿงช Let's test it out!
type Test1 = MyConditional<string>;     // "It's a string! ๐Ÿ“"
type Test2 = MyConditional<number>;     // "Not a string ๐Ÿคทโ€โ™€๏ธ"
type Test3 = MyConditional<boolean>;    // "Not a string ๐Ÿคทโ€โ™€๏ธ"

๐Ÿ’ก The Magic Formula

The conditional type syntax follows this pattern:

T extends U ? X : Y

This reads as: โ€œDoes T extend U? If yes, give me X. If no, give me Y.โ€

  • ๐ŸŽฏ T: The type weโ€™re checking
  • ๐Ÿ” U: The type weโ€™re comparing against
  • โœ… X: What to return if T extends U (true case)
  • โŒ Y: What to return if T doesnโ€™t extend U (false case)

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Your First Conditional Types

Letโ€™s start with some friendly examples:

// ๐ŸŽจ Check if a type is an array
type IsArray<T> = T extends any[] ? true : false;

// ๐Ÿงช Testing our array detector
type Test1 = IsArray<string[]>;   // true โœ…
type Test2 = IsArray<number>;     // false โŒ
type Test3 = IsArray<boolean[]>;  // true โœ…

// ๐Ÿ›’ Extract array element type
type ArrayElement<T> = T extends (infer U)[] ? U : never;

// ๐ŸŽฎ Let's see it in action!
type ProductType = ArrayElement<string[]>;     // string
type ScoreType = ArrayElement<number[]>;       // number
type MysteryType = ArrayElement<boolean>;      // never

๐Ÿ’ก Pro Tip: The infer keyword is like a detective ๐Ÿ•ต๏ธโ€โ™€๏ธ - it captures and extracts type information!

๐ŸŽฏ Practical Everyday Examples

// ๐Ÿš€ Remove null and undefined from types
type NonNullable<T> = T extends null | undefined ? never : T;

// ๐Ÿงช Testing our null remover
type CleanString = NonNullable<string | null>;      // string
type CleanNumber = NonNullable<number | undefined>; // number
type SuperClean = NonNullable<boolean | null | undefined>; // boolean

// ๐Ÿ“ฆ Extract function return types
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// ๐ŸŽฎ Testing with different functions
type LoginResult = ReturnType<() => boolean>;                    // boolean
type UserData = ReturnType<(id: string) => { name: string }>;   // { name: string }
type VoidResult = ReturnType<(x: number) => void>;              // void

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Smart E-Commerce Types

Letโ€™s build a type-safe shopping system:

// ๐Ÿช Define our product types
interface Product {
  id: string;
  name: string;
  price: number;
  category: "electronics" | "clothing" | "books";
}

interface DigitalProduct extends Product {
  downloadLink: string;
  fileSize: number;
}

interface PhysicalProduct extends Product {
  weight: number;
  dimensions: { width: number; height: number; depth: number };
}

// ๐ŸŽฏ Smart shipping calculator that adapts to product type
type ShippingMethod<T> = T extends DigitalProduct 
  ? "instant-download ๐Ÿ“ง" 
  : T extends PhysicalProduct 
    ? "standard-shipping ๐Ÿ“ฆ" 
    : "unknown ๐Ÿคทโ€โ™€๏ธ";

// ๐Ÿงช Let's test our smart shipping!
type EbookShipping = ShippingMethod<DigitalProduct>;    // "instant-download ๐Ÿ“ง"
type BookShipping = ShippingMethod<PhysicalProduct>;    // "standard-shipping ๐Ÿ“ฆ"

// ๐ŸŽจ Smart price calculator
type PriceDisplay<T> = T extends DigitalProduct 
  ? { price: number; currency: string; instant: true }
  : T extends PhysicalProduct 
    ? { price: number; currency: string; shippingCost: number }
    : { price: number; currency: string };

// ๐ŸŽฎ Usage example
class ShoppingCart<T extends Product> {
  constructor(private product: T) {}
  
  // ๐Ÿš€ Method that adapts based on product type
  getShippingInfo(): ShippingMethod<T> {
    return (this.product as any).downloadLink 
      ? ("instant-download ๐Ÿ“ง" as ShippingMethod<T>)
      : ("standard-shipping ๐Ÿ“ฆ" as ShippingMethod<T>);
  }
}

// โœจ The magic happens here!
const digitalCart = new ShoppingCart<DigitalProduct>({
  id: "1",
  name: "TypeScript Mastery Course",
  price: 49.99,
  category: "books",
  downloadLink: "https://course.com/download",
  fileSize: 2048
});

console.log(digitalCart.getShippingInfo()); // "instant-download ๐Ÿ“ง"

๐ŸŽฎ Example 2: Game State Management

Letโ€™s create an adaptive game system:

// ๐Ÿ† Game state interfaces
interface MenuState {
  type: "menu";
  currentMenu: "main" | "settings" | "leaderboard";
  backgroundMusic: boolean;
}

interface PlayingState {
  type: "playing";
  level: number;
  score: number;
  lives: number;
  powerUps: string[];
}

interface PausedState {
  type: "paused";
  savedState: PlayingState;
  pauseTime: Date;
}

interface GameOverState {
  type: "gameOver";
  finalScore: number;
  newHighScore: boolean;
  achievements: string[];
}

// ๐ŸŽฏ Smart UI components based on game state
type GameUI<T> = T extends MenuState
  ? { showMenu: true; showGame: false; showPause: false }
  : T extends PlayingState
    ? { showMenu: false; showGame: true; showPause: false }
    : T extends PausedState
      ? { showMenu: false; showGame: true; showPause: true }
      : T extends GameOverState
        ? { showMenu: true; showGame: false; showPause: false; showGameOver: true }
        : never;

// ๐ŸŽจ Smart actions based on state
type AvailableActions<T> = T extends MenuState
  ? "startGame" | "openSettings" | "viewLeaderboard"
  : T extends PlayingState
    ? "pause" | "useItem" | "move" | "attack"
    : T extends PausedState
      ? "resume" | "mainMenu" | "restart"
      : T extends GameOverState
        ? "restart" | "mainMenu" | "shareScore"
        : never;

// ๐Ÿš€ Game manager class
class GameManager<T extends MenuState | PlayingState | PausedState | GameOverState> {
  constructor(private state: T) {}
  
  // ๐ŸŽฏ UI configuration adapts to current state
  getUIConfig(): GameUI<T> {
    switch (this.state.type) {
      case "menu":
        return { showMenu: true, showGame: false, showPause: false } as GameUI<T>;
      case "playing":
        return { showMenu: false, showGame: true, showPause: false } as GameUI<T>;
      case "paused":
        return { showMenu: false, showGame: true, showPause: true } as GameUI<T>;
      case "gameOver":
        return { showMenu: true, showGame: false, showPause: false, showGameOver: true } as GameUI<T>;
      default:
        throw new Error("Unknown game state ๐Ÿ˜ฑ");
    }
  }
  
  // ๐Ÿ’ก Available actions adapt to state
  getAvailableActions(): AvailableActions<T>[] {
    // Implementation would return appropriate actions based on state
    return [] as AvailableActions<T>[];
  }
}

// ๐ŸŽฎ Usage examples
const menuManager = new GameManager<MenuState>({
  type: "menu",
  currentMenu: "main",
  backgroundMusic: true
});

const playingManager = new GameManager<PlayingState>({
  type: "playing",
  level: 5,
  score: 15000,
  lives: 3,
  powerUps: ["๐Ÿš€ Speed Boost", "๐Ÿ›ก๏ธ Shield", "๐Ÿ’ฅ Double Damage"]
});

console.log("๐ŸŽฏ Menu UI:", menuManager.getUIConfig());
console.log("๐ŸŽฎ Playing UI:", playingManager.getUIConfig());

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Nested Conditional Types

When youโ€™re ready to level up, try chaining conditionals:

// ๐ŸŽจ Multi-level type checking
type DeepTypeCheck<T> = 
  T extends string 
    ? T extends `${string}@${string}.${string}` 
      ? "Valid email! ๐Ÿ“ง" 
      : "String but not email ๐Ÿ“"
    : T extends number
      ? T extends 0
        ? "Zero! ๐Ÿšซ"
        : "Non-zero number! ๐Ÿ”ข"
      : T extends boolean
        ? T extends true
          ? "Truthy! โœ…"
          : "Falsy! โŒ"
        : "Unknown type! ๐Ÿคทโ€โ™€๏ธ";

// ๐Ÿงช Testing our deep checker
type EmailTest = DeepTypeCheck<"[email protected]">;  // "Valid email! ๐Ÿ“ง"
type StringTest = DeepTypeCheck<"hello">;            // "String but not email ๐Ÿ“"
type ZeroTest = DeepTypeCheck<0>;                    // "Zero! ๐Ÿšซ"
type NumberTest = DeepTypeCheck<42>;                 // "Non-zero number! ๐Ÿ”ข"
type BooleanTest = DeepTypeCheck<true>;              // "Truthy! โœ…"

๐Ÿ—๏ธ Building Custom Utility Types

Create your own TypeScript superpowers:

// ๐ŸŽฏ Extract all function property names from an object
type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

// ๐Ÿ›’ Extract all non-function property names
type NonFunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];

// ๐ŸŽจ Create function-only and data-only types
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

// ๐Ÿงช Testing with a user class
class User {
  name: string = "Alice";
  age: number = 30;
  email: string = "[email protected]";
  
  greet(): string { return `Hello, I'm ${this.name}! ๐Ÿ‘‹`; }
  getAge(): number { return this.age; }
  updateEmail(newEmail: string): void { this.email = newEmail; }
}

// โœจ The magic in action!
type UserMethods = FunctionProperties<User>;
// Result: { greet(): string; getAge(): number; updateEmail(newEmail: string): void; }

type UserData = NonFunctionProperties<User>;
// Result: { name: string; age: number; email: string; }

// ๐Ÿš€ Advanced: Extract promise return types
type PromiseType<T> = T extends Promise<infer U> ? U : T;

// ๐ŸŽฎ Testing promise extraction
type ApiResponse = PromiseType<Promise<{ data: string[] }>>;  // { data: string[] }
type DirectValue = PromiseType<string>;                      // string

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: The โ€œDistributedโ€ Trap

// โŒ This might not work as expected with union types!
type BadExample<T> = T extends string ? "string" : "not string";

// ๐Ÿงช Testing with union type
type UnionTest = BadExample<string | number>;  // "string" | "not string" (distributed!)

// โœ… Prevent distribution with brackets
type GoodExample<T> = [T] extends [string] ? "string" : "not string";

type BetterUnionTest = GoodExample<string | number>;  // "not string" (not distributed)

๐Ÿ’ก Explanation: Conditional types distribute over union types by default. Use brackets [T] to prevent this behavior!

๐Ÿคฏ Pitfall 2: Infinite Recursion

// โŒ Dangerous - can cause infinite recursion!
type BadRecursive<T> = T extends any[] 
  ? BadRecursive<T[0]>  // ๐Ÿ’ฅ This might never end!
  : T;

// โœ… Safe recursive types with depth limits
type SafeRecursive<T, Depth extends readonly any[] = []> = 
  Depth['length'] extends 10  // ๐Ÿ›ก๏ธ Stop at depth 10
    ? T
    : T extends any[]
      ? SafeRecursive<T[0], [...Depth, any]>
      : T;

// ๐Ÿงช Testing our safe recursion
type DeepArrayTest = SafeRecursive<string[][][]>;  // string (safely extracted)

๐Ÿ” Pitfall 3: The never Mystery

// โŒ Forgetting about the never case
type IncompleteType<T> = T extends string ? string : T extends number ? number : boolean;

// ๐Ÿงช What happens with other types?
type SymbolTest = IncompleteType<symbol>;  // boolean (probably not what we wanted!)

// โœ… Handle all cases explicitly
type CompleteType<T> = T extends string 
  ? string 
  : T extends number 
    ? number 
    : T extends boolean 
      ? boolean 
      : never;  // ๐ŸŽฏ Explicit handling of unexpected types

type BetterSymbolTest = CompleteType<symbol>;  // never (much clearer!)

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Be Explicit: Always handle the false case clearly
  2. ๐Ÿ“ Use Meaningful Names: IsArray<T> is better than Check<T>
  3. ๐Ÿ›ก๏ธ Prevent Infinite Recursion: Set depth limits for recursive types
  4. ๐ŸŽจ Leverage Distribution: Understand when unions distribute and when they donโ€™t
  5. โœจ Keep It Simple: Donโ€™t over-engineer your conditional types
  6. ๐Ÿ” Test Edge Cases: Always test with never, unknown, and union types

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Smart API Response Type System

Create a type system that handles different API response patterns:

๐Ÿ“‹ Requirements:

  • โœ… Success responses with data
  • โŒ Error responses with error messages
  • ๐Ÿ“Š Paginated responses with metadata
  • ๐Ÿ” Loading states
  • ๐ŸŽจ Each response type needs appropriate properties!

๐Ÿš€ Bonus Points:

  • Add request method detection (GET, POST, etc.)
  • Create response validators
  • Build a type-safe API client interface

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ API Response Types
interface ApiSuccess<T = any> {
  status: "success";
  data: T;
  timestamp: string;
}

interface ApiError {
  status: "error";
  message: string;
  code: number;
  details?: string[];
}

interface ApiPaginated<T = any> {
  status: "success";
  data: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    hasMore: boolean;
  };
}

interface ApiLoading {
  status: "loading";
  progress?: number;
}

// ๐Ÿš€ Smart response type detector
type ApiResponse<T> = ApiSuccess<T> | ApiError | ApiPaginated<T> | ApiLoading;

// ๐ŸŽจ Extract data type from response
type ResponseData<T> = T extends ApiSuccess<infer U>
  ? U
  : T extends ApiPaginated<infer U>
    ? U[]
    : T extends ApiError
      ? never
      : T extends ApiLoading
        ? never
        : never;

// ๐Ÿ›ก๏ธ Type guards for runtime checking
const isSuccess = <T>(response: ApiResponse<T>): response is ApiSuccess<T> => 
  response.status === "success" && "data" in response && !("pagination" in response);

const isError = <T>(response: ApiResponse<T>): response is ApiError => 
  response.status === "error";

const isPaginated = <T>(response: ApiResponse<T>): response is ApiPaginated<T> => 
  response.status === "success" && "pagination" in response;

const isLoading = <T>(response: ApiResponse<T>): response is ApiLoading => 
  response.status === "loading";

// ๐ŸŽฏ Smart API client
class SmartApiClient {
  async get<T>(url: string): Promise<ApiResponse<T>> {
    try {
      // ๐Ÿ“ก Simulate API call
      const mockResponse: ApiSuccess<T> = {
        status: "success",
        data: { message: "Hello TypeScript! ๐ŸŽ‰" } as T,
        timestamp: new Date().toISOString()
      };
      
      return mockResponse;
    } catch (error) {
      return {
        status: "error",
        message: "Request failed ๐Ÿ˜ข",
        code: 500
      };
    }
  }
  
  // ๐ŸŽฎ Smart response handler
  handleResponse<T>(response: ApiResponse<T>): void {
    if (isSuccess(response)) {
      console.log("โœ… Success:", response.data);
      console.log("๐Ÿ• Timestamp:", response.timestamp);
    } else if (isError(response)) {
      console.log("โŒ Error:", response.message);
      console.log("๐Ÿ”ข Code:", response.code);
    } else if (isPaginated(response)) {
      console.log("๐Ÿ“Š Data:", response.data);
      console.log("๐Ÿ“„ Page:", response.pagination.page);
      console.log("๐Ÿ“ˆ Total:", response.pagination.total);
    } else if (isLoading(response)) {
      console.log("โณ Loading...");
      if (response.progress) {
        console.log("๐Ÿ“Š Progress:", `${response.progress}%`);
      }
    }
  }
}

// ๐Ÿงช Testing our smart API client
const apiClient = new SmartApiClient();

// ๐ŸŽฎ Type-safe usage
type UserData = { id: number; name: string; email: string };

apiClient.get<UserData>("/users/123").then(response => {
  // TypeScript knows exactly what response can be!
  apiClient.handleResponse(response);
  
  // ๐ŸŽฏ Type-safe data extraction
  if (isSuccess(response)) {
    // TypeScript knows response.data is UserData
    console.log(`๐Ÿ‘ค User: ${response.data.name}`);
    console.log(`๐Ÿ“ง Email: ${response.data.email}`);
  }
});

// โœจ Advanced: Extract response data type
type ExtractedUserData = ResponseData<ApiSuccess<UserData>>;  // UserData
type ExtractedErrorData = ResponseData<ApiError>;            // never

๐ŸŽ“ Key Takeaways

Youโ€™ve conquered conditional types! Hereโ€™s what you can now do:

  • โœ… Create smart, adaptive types that make decisions ๐Ÿ’ช
  • โœ… Build custom utility types for your specific needs ๐Ÿ›ก๏ธ
  • โœ… Handle complex type transformations with confidence ๐ŸŽฏ
  • โœ… Debug type-level logic like a pro ๐Ÿ›
  • โœ… Leverage TypeScriptโ€™s most powerful features ๐Ÿš€

Remember: Conditional types are like having a crystal ball ๐Ÿ”ฎ for your types - they can predict and adapt to any situation!

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered conditional types!

Hereโ€™s what to explore next:

  1. ๐Ÿ’ป Practice with the exercise above - try different API patterns
  2. ๐Ÿ—๏ธ Build a library using advanced conditional types
  3. ๐Ÿ“š Move on to our next tutorial: Distributed Conditional Types: Advanced Patterns
  4. ๐ŸŒŸ Share your conditional type creations with the community!

Youโ€™re now equipped with one of TypeScriptโ€™s most powerful weapons. Use it wisely, and remember - every type wizard was once a beginner! Keep experimenting, keep learning, and most importantly, have fun with types! ๐Ÿš€โœจ


Happy type-level programming! ๐ŸŽ‰๐Ÿ”ฎโœจ