+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 65 of 354

๐Ÿš€ Distributed Conditional Types: Advanced Pattern Magic

Master the power of distributed conditional types to transform union types with precision and elegance ๐ŸŽจ

๐Ÿš€Intermediate
30 min read

Prerequisites

  • Understanding of basic conditional types ๐ŸŽฏ
  • Knowledge of union types ๐Ÿ“
  • Familiarity with type inference โšก

What you'll learn

  • Master distributed conditional type behavior ๐Ÿ—๏ธ
  • Control distribution with advanced techniques ๐Ÿ”ง
  • Build complex type transformations ๐ŸŽจ
  • Debug distribution issues like a pro ๐Ÿ›

๐ŸŽฏ Introduction

Welcome to the advanced world of distributed conditional types! ๐ŸŽ‰ If regular conditional types are like smart filters ๐ŸŽจ, then distributed conditionals are like magic wands โœจ that can transform entire collections of types with a single spell!

Youโ€™re about to discover one of TypeScriptโ€™s most elegant features - the ability to automatically apply conditional logic to every member of a union type. Itโ€™s like having a personal assistant ๐Ÿค– that handles all the repetitive type transformations for you.

By the end of this tutorial, youโ€™ll be wielding distributed conditionals like a type wizard ๐Ÿง™โ€โ™‚๏ธ, creating elegant solutions that would otherwise require complex manual mapping. Letโ€™s dive into this magical journey! ๐ŸŒŸ

๐Ÿ“š Understanding Distribution

๐Ÿค” What is Distribution?

Think of distribution like a magical photocopier ๐Ÿ“„โœจ. When you have a union type like string | number | boolean and apply a conditional type, TypeScript automatically โ€œcopiesโ€ the conditional logic to each member of the union:

// ๐ŸŽฏ This conditional type...
type Stringify<T> = T extends any ? `${T}` : never;

// ๐Ÿช„ Applied to a union...
type UnionTest = Stringify<string | number | boolean>;

// โœจ Becomes this (distributed automatically!)
type Result = Stringify<string> | Stringify<number> | Stringify<boolean>;
// Which evaluates to: string | string | string

๐Ÿ’ก The Magic: TypeScript sees the union and thinks โ€œLet me apply this conditional to each member separately!โ€

๐ŸŽจ Distribution in Action

Letโ€™s see the magic happening:

// ๐Ÿงช Simple distribution example
type AddArray<T> = T extends any ? T[] : never;

// ๐ŸŽฎ Testing with unions
type SingleTypes = string | number | boolean;
type ArrayTypes = AddArray<SingleTypes>;
// Result: string[] | number[] | boolean[]

// ๐Ÿš€ More complex example
type WrapInPromise<T> = T extends any ? Promise<T> : never;

type SyncTypes = string | number | { name: string };
type AsyncTypes = WrapInPromise<SyncTypes>;
// Result: Promise<string> | Promise<number> | Promise<{ name: string }>

๐ŸŽฏ Key Insight: Distribution happens automatically when you use T extends SomeType where T is a union type!

๐Ÿ”ง Basic Distribution Patterns

๐Ÿ“ Your First Distributed Types

Letโ€™s explore common distribution patterns:

// ๐ŸŽจ Extract all string types from a union
type ExtractStrings<T> = T extends string ? T : never;

// ๐Ÿงช Testing string extraction
type MixedUnion = string | number | "hello" | 42 | boolean;
type OnlyStrings = ExtractStrings<MixedUnion>;
// Result: string | "hello"

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

// ๐ŸŽฎ Clean up messy types
type MessyType = string | null | number | undefined | boolean;
type CleanType = NonNullable<MessyType>;
// Result: string | number | boolean

// ๐Ÿ›’ Transform types based on their nature
type Serialize<T> = T extends string
  ? T  // Keep strings as-is
  : T extends number
    ? string  // Convert numbers to strings
    : T extends boolean
      ? "true" | "false"  // Convert booleans to string literals
      : string;  // Everything else becomes string

// โœจ Watch the magic happen!
type SerializeTest = Serialize<string | number | boolean | Date>;
// Result: string | string | "true" | "false" | string

๐ŸŽฏ Advanced Distribution Tricks

// ๐ŸŽจ Extract function return types from a union of functions
type ReturnTypes<T> = T extends (...args: any[]) => infer R ? R : never;

// ๐Ÿงช Testing with function union
type FunctionUnion = 
  | (() => string)
  | ((x: number) => boolean)
  | ((a: string, b: number) => Date);

type AllReturns = ReturnTypes<FunctionUnion>;
// Result: string | boolean | Date

// ๐Ÿš€ Extract all possible property values from object types
type PropertyValues<T> = T extends { [K in keyof T]: infer V } ? V : never;

// ๐ŸŽฎ Testing with object union
type ObjectUnion = 
  | { name: string; age: number }
  | { title: string; pages: number }
  | { brand: string; price: number };

type AllValues = PropertyValues<ObjectUnion>;
// Result: string | number (all possible property value types)

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Smart Event System

Letโ€™s build a type-safe event system using distribution:

// ๐ŸŽฏ Define our event types
interface UserEvents {
  "user:login": { userId: string; timestamp: Date };
  "user:logout": { userId: string; sessionDuration: number };
  "user:profile-update": { userId: string; changes: Record<string, any> };
}

interface ProductEvents {
  "product:created": { productId: string; name: string; price: number };
  "product:updated": { productId: string; changes: Partial<Product> };
  "product:deleted": { productId: string };
}

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
}

// ๐Ÿš€ Combine all events
type AllEvents = UserEvents & ProductEvents;

// ๐ŸŽจ Extract all event names using distribution
type EventNames<T> = T extends Record<infer K, any> ? K : never;
type AllEventNames = EventNames<keyof AllEvents>;
// Result: all event name strings

// โœจ Smart event handler type that distributes over all events
type EventHandler<T extends keyof AllEvents> = T extends any
  ? (eventName: T, payload: AllEvents[T]) => void
  : never;

// ๐Ÿงช Create specific handlers for each event type
type UserLoginHandler = EventHandler<"user:login">;
// Result: (eventName: "user:login", payload: { userId: string; timestamp: Date }) => void

type ProductCreatedHandler = EventHandler<"product:created">;
// Result: (eventName: "product:created", payload: { productId: string; name: string; price: number }) => void

// ๐ŸŽฎ Event emitter class with distributed types
class SmartEventEmitter {
  private handlers: Map<keyof AllEvents, Function[]> = new Map();

  // ๐Ÿš€ Type-safe event registration with distribution
  on<T extends keyof AllEvents>(
    eventName: T,
    handler: EventHandler<T>
  ): void {
    const existingHandlers = this.handlers.get(eventName) || [];
    this.handlers.set(eventName, [...existingHandlers, handler]);
    console.log(`๐Ÿ“ก Registered handler for ${String(eventName)}`);
  }

  // โœจ Type-safe event emission
  emit<T extends keyof AllEvents>(
    eventName: T,
    payload: AllEvents[T]
  ): void {
    const handlers = this.handlers.get(eventName) || [];
    handlers.forEach(handler => {
      (handler as EventHandler<T>)(eventName, payload);
    });
    console.log(`๐Ÿš€ Emitted ${String(eventName)} with payload:`, payload);
  }
}

// ๐ŸŽฏ Usage example
const eventEmitter = new SmartEventEmitter();

// ๐Ÿ’ก TypeScript knows exactly what payload type each event expects!
eventEmitter.on("user:login", (eventName, payload) => {
  console.log(`๐Ÿ‘‹ User ${payload.userId} logged in at ${payload.timestamp}`);
});

eventEmitter.on("product:created", (eventName, payload) => {
  console.log(`๐Ÿ›๏ธ New product: ${payload.name} ($${payload.price})`);
});

// โœ… Type-safe emission
eventEmitter.emit("user:login", {
  userId: "user123",
  timestamp: new Date()
});

eventEmitter.emit("product:created", {
  productId: "prod456",
  name: "TypeScript Guide",
  price: 29.99
});

๐ŸŽฎ Example 2: API Response Transformer

Letโ€™s create a distributed type system for API transformations:

// ๐Ÿ† Define different API response types
interface ApiUser {
  type: "user";
  id: string;
  name: string;
  email: string;
  createdAt: string;  // ISO date string
}

interface ApiProduct {
  type: "product";
  id: string;
  title: string;
  price_cents: number;  // Price in cents
  created_date: string;  // Different date format
}

interface ApiOrder {
  type: "order";
  order_id: string;
  user_id: string;
  total_amount: number;
  status: "pending" | "confirmed" | "shipped";
}

// ๐ŸŽฏ Union of all API response types
type ApiResponse = ApiUser | ApiProduct | ApiOrder;

// ๐Ÿš€ Distributed transformation to frontend types
type TransformToFrontend<T> = T extends ApiUser
  ? {
      type: "user";
      id: string;
      name: string;
      email: string;
      createdAt: Date;  // Transform to Date object
    }
  : T extends ApiProduct
    ? {
        type: "product";
        id: string;
        title: string;
        price: number;  // Transform cents to dollars
        createdAt: Date;  // Normalize date field name
      }
    : T extends ApiOrder
      ? {
          type: "order";
          id: string;  // Normalize field name
          userId: string;  // Normalize field name
          totalAmount: number;
          status: "pending" | "confirmed" | "shipped";
        }
      : never;

// โœจ Apply transformation to all response types
type FrontendResponse = TransformToFrontend<ApiResponse>;
// Result: Frontend version of all three types unioned together

// ๐ŸŽจ Smart transformer function
function transformApiResponse<T extends ApiResponse>(
  apiData: T
): TransformToFrontend<T> {
  switch (apiData.type) {
    case "user":
      return {
        type: "user",
        id: apiData.id,
        name: apiData.name,
        email: apiData.email,
        createdAt: new Date(apiData.createdAt)
      } as TransformToFrontend<T>;
    
    case "product":
      return {
        type: "product",
        id: apiData.id,
        title: apiData.title,
        price: apiData.price_cents / 100,  // Convert cents to dollars
        createdAt: new Date(apiData.created_date)
      } as TransformToFrontend<T>;
    
    case "order":
      return {
        type: "order",
        id: apiData.order_id,
        userId: apiData.user_id,
        totalAmount: apiData.total_amount,
        status: apiData.status
      } as TransformToFrontend<T>;
    
    default:
      throw new Error(`Unknown API response type ๐Ÿ˜ฑ`);
  }
}

// ๐Ÿงช Usage examples
const apiUser: ApiUser = {
  type: "user",
  id: "user123",
  name: "Alice Wonder",
  email: "[email protected]",
  createdAt: "2023-12-01T10:30:00Z"
};

const apiProduct: ApiProduct = {
  type: "product",
  id: "prod456",
  title: "TypeScript Mastery Course",
  price_cents: 4999,  // $49.99 in cents
  created_date: "2023-12-01T15:45:00Z"
};

// ๐ŸŽฏ Transform with full type safety
const frontendUser = transformApiResponse(apiUser);
console.log(`๐Ÿ‘ค User: ${frontendUser.name}, joined: ${frontendUser.createdAt.getFullYear()}`);

const frontendProduct = transformApiResponse(apiProduct);
console.log(`๐Ÿ›๏ธ Product: ${frontendProduct.title}, price: $${frontendProduct.price}`);

๐Ÿš€ Advanced Distribution Control

๐Ÿง™โ€โ™‚๏ธ Preventing Distribution

Sometimes you donโ€™t want distribution to happen:

// โŒ This distributes (might not be what we want)
type Distributed<T> = T extends any ? T[] : never;
type DistributedTest = Distributed<string | number>;  // string[] | number[]

// โœ… Prevent distribution with tuple wrapping
type NonDistributed<T> = [T] extends [any] ? T[] : never;
type NonDistributedTest = NonDistributed<string | number>;  // (string | number)[]

// ๐ŸŽฏ Practical example: Ensure all union members have same structure
type EnsureSameShape<T> = [T] extends [{ id: string }] 
  ? T 
  : never;  // Forces all union members to have id property

// ๐Ÿงช Testing shape enforcement
type GoodUnion = 
  | { id: string; name: string }
  | { id: string; age: number };

type BadUnion = 
  | { id: string; name: string }
  | { name: string; age: number };  // Missing id in second type

type GoodResult = EnsureSameShape<GoodUnion>;  // Works fine
type BadResult = EnsureSameShape<BadUnion>;    // Results in never

๐Ÿ—๏ธ Advanced Distribution Patterns

// ๐ŸŽจ Distribute with conditions based on structure
type SmartDistribute<T> = T extends { type: infer U }
  ? U extends string
    ? { kind: U; data: Omit<T, "type"> }
    : T
  : T;

// ๐Ÿงช Testing smart distribution
type ObjectWithType = 
  | { type: "user"; name: string; email: string }
  | { type: "product"; title: string; price: number }
  | { age: number };  // No type property

type SmartResult = SmartDistribute<ObjectWithType>;
// Result transforms typed objects but leaves others alone

// ๐Ÿš€ Chain multiple distributions
type ChainedTransform<T> = 
  T extends string
    ? `String: ${T}`
    : T extends number
      ? T extends 0
        ? "Zero"
        : `Number: ${T}`
      : T extends boolean
        ? T extends true
          ? "Truthy"
          : "Falsy"
        : "Unknown";

// ๐ŸŽฎ Testing chained distribution
type ChainTest = ChainedTransform<string | 0 | 42 | true | false | symbol>;
// Each union member gets its own transformation path!

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Unexpected Distribution

// โŒ Unexpected behavior - distribution can surprise you!
type MakeArray<T> = T[];  // This doesn't distribute!
type ArrayTest1 = MakeArray<string | number>;  // (string | number)[]

type MakeArrayConditional<T> = T extends any ? T[] : never;  // This does!
type ArrayTest2 = MakeArrayConditional<string | number>;  // string[] | number[]

// โœ… Be explicit about your intentions
type ExplicitArray<T> = [T] extends [any] ? T[] : never;  // Single array
type ExplicitDistribute<T> = T extends any ? T[] : never;  // Distributed arrays

๐Ÿคฏ Pitfall 2: Never Types in Distribution

// โŒ Never types can disappear in unions!
type FilterOut<T> = T extends string ? never : T;

type TestUnion = string | number | boolean;
type FilteredUnion = FilterOut<TestUnion>;  // number | boolean (string filtered out)

// โœ… Understanding never behavior
type DebugFilter<T> = T extends string 
  ? { filtered: true; type: T }
  : { filtered: false; type: T };

type DebugResult = DebugFilter<string | number>;
// Shows exactly what happens to each union member

๐Ÿ” Pitfall 3: Complex Nested Distribution

// โŒ Complex nesting can be confusing
type ComplexDistribute<T> = T extends { items: infer U }
  ? U extends any[]
    ? U[0] extends infer V
      ? V extends { id: any }
        ? V
        : never
      : never
    : never
  : never;

// โœ… Break it down into simpler steps
type ExtractItems<T> = T extends { items: infer U } ? U : never;
type GetFirstItem<T> = T extends any[] ? T[0] : never;
type FilterWithId<T> = T extends { id: any } ? T : never;

type CleanerDistribute<T> = FilterWithId<GetFirstItem<ExtractItems<T>>>;
// Much clearer what each step does!

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Understand When Distribution Happens: Only in T extends... conditionals
  2. ๐Ÿ“ Control Distribution Explicitly: Use [T] to prevent, conditional types to enable
  3. ๐Ÿ›ก๏ธ Handle Never Types: Know that never disappears from unions
  4. ๐ŸŽจ Break Down Complex Logic: Use intermediate types for clarity
  5. โœจ Test Edge Cases: Always test with never, single types, and complex unions
  6. ๐Ÿ” Use Type Debugging: Create debug types to understand whatโ€™s happening

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Smart Form Validation System

Create a distributed type system that handles different form field types:

๐Ÿ“‹ Requirements:

  • โœ… Different field types: text, email, number, date, select
  • ๐Ÿ”ง Each field type has different validation rules
  • ๐ŸŽจ Error messages adapt to field type
  • ๐Ÿ“Š Form state tracks all field types
  • โœจ Type-safe form submission!

๐Ÿš€ Bonus Points:

  • Add conditional required/optional fields
  • Create field dependency validation
  • Build a form wizard with step-by-step validation

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Define field types
interface TextField {
  type: "text";
  name: string;
  value: string;
  minLength?: number;
  maxLength?: number;
  pattern?: string;
}

interface EmailField {
  type: "email";
  name: string;
  value: string;
  domain?: string;  // Optional domain restriction
}

interface NumberField {
  type: "number";
  name: string;
  value: number;
  min?: number;
  max?: number;
  step?: number;
}

interface DateField {
  type: "date";
  name: string;
  value: Date;
  minDate?: Date;
  maxDate?: Date;
}

interface SelectField {
  type: "select";
  name: string;
  value: string;
  options: string[];
  multiple?: boolean;
}

// ๐Ÿš€ Union of all field types
type FormField = TextField | EmailField | NumberField | DateField | SelectField;

// ๐ŸŽจ Distributed validation error types
type ValidationError<T extends FormField> = T extends TextField
  ? {
      type: "text";
      field: string;
      errors: ("too_short" | "too_long" | "invalid_pattern")[];
    }
  : T extends EmailField
    ? {
        type: "email";
        field: string;
        errors: ("invalid_format" | "invalid_domain")[];
      }
    : T extends NumberField
      ? {
          type: "number";
          field: string;
          errors: ("too_small" | "too_large" | "invalid_step")[];
        }
      : T extends DateField
        ? {
            type: "date";
            field: string;
            errors: ("too_early" | "too_late" | "invalid_date")[];
          }
        : T extends SelectField
          ? {
              type: "select";
              field: string;
              errors: ("invalid_option" | "required_selection")[];
            }
          : never;

// โœจ Smart validator that distributes over field types
type FieldValidator<T extends FormField> = (field: T) => ValidationError<T> | null;

// ๐Ÿงช Individual validators
const validateTextField: FieldValidator<TextField> = (field) => {
  const errors: ("too_short" | "too_long" | "invalid_pattern")[] = [];
  
  if (field.minLength && field.value.length < field.minLength) {
    errors.push("too_short");
  }
  if (field.maxLength && field.value.length > field.maxLength) {
    errors.push("too_long");
  }
  if (field.pattern && !new RegExp(field.pattern).test(field.value)) {
    errors.push("invalid_pattern");
  }
  
  return errors.length > 0 ? { type: "text", field: field.name, errors } : null;
};

const validateEmailField: FieldValidator<EmailField> = (field) => {
  const errors: ("invalid_format" | "invalid_domain")[] = [];
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  
  if (!emailRegex.test(field.value)) {
    errors.push("invalid_format");
  }
  if (field.domain && !field.value.endsWith(`@${field.domain}`)) {
    errors.push("invalid_domain");
  }
  
  return errors.length > 0 ? { type: "email", field: field.name, errors } : null;
};

// ๐ŸŽฎ Smart form validator
class SmartFormValidator {
  // ๐Ÿš€ Generic validation that distributes
  validate<T extends FormField>(field: T): ValidationError<T> | null {
    switch (field.type) {
      case "text":
        return validateTextField(field as TextField) as ValidationError<T> | null;
      case "email":
        return validateEmailField(field as EmailField) as ValidationError<T> | null;
      case "number":
        // Implementation for number validation
        return null;
      case "date":
        // Implementation for date validation
        return null;
      case "select":
        // Implementation for select validation
        return null;
      default:
        return null;
    }
  }
  
  // โœจ Validate entire form with distributed types
  validateForm<T extends FormField[]>(fields: T): ValidationError<T[number]>[] {
    return fields
      .map(field => this.validate(field))
      .filter((error): error is ValidationError<T[number]> => error !== null);
  }
}

// ๐ŸŽฏ Usage example
const validator = new SmartFormValidator();

const formFields: FormField[] = [
  {
    type: "text",
    name: "username",
    value: "alice",
    minLength: 3,
    maxLength: 20
  },
  {
    type: "email",
    name: "email",
    value: "[email protected]",
    domain: "company.com"  // This will cause a validation error
  },
  {
    type: "number",
    name: "age",
    value: 25,
    min: 18,
    max: 100
  }
];

// ๐Ÿงช Validate with full type safety
const errors = validator.validateForm(formFields);
console.log("๐Ÿ” Validation errors:", errors);

// โœ… Each error has the correct type structure for its field type!
errors.forEach(error => {
  switch (error.type) {
    case "text":
      console.log(`๐Ÿ“ Text field ${error.field}:`, error.errors);
      break;
    case "email":
      console.log(`๐Ÿ“ง Email field ${error.field}:`, error.errors);
      break;
    // TypeScript ensures we handle all possible error types!
  }
});

๐ŸŽ“ Key Takeaways

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

  • โœ… Understand automatic distribution in union types ๐Ÿ’ช
  • โœ… Control distribution behavior when needed ๐Ÿ›ก๏ธ
  • โœ… Build elegant type transformations that scale ๐ŸŽฏ
  • โœ… Debug complex distribution patterns like a pro ๐Ÿ›
  • โœ… Create type-safe systems that adapt automatically ๐Ÿš€

Remember: Distributed conditionals are like having a magic wand โœจ that can transform entire collections of types with elegant precision!

๐Ÿค Next Steps

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

Hereโ€™s what to explore next:

  1. ๐Ÿ’ป Practice with the form validation exercise above
  2. ๐Ÿ—๏ธ Build a distributed type system for your own project
  3. ๐Ÿ“š Move on to our next tutorial: Infer Keyword: Extracting Types in Conditionals
  4. ๐ŸŒŸ Share your distributed type patterns with the community!

You now wield one of TypeScriptโ€™s most powerful and elegant features. Use this knowledge to create type systems that are both powerful and maintainable. Remember - every advanced TypeScript developer started with the basics. Keep experimenting, keep learning, and most importantly, enjoy the magic of types! ๐Ÿš€โœจ


Happy distributed programming! ๐ŸŽ‰๐Ÿช„โœจ