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
- ๐ฏ Understand When Distribution Happens: Only in
T extends...
conditionals - ๐ Control Distribution Explicitly: Use
[T]
to prevent, conditional types to enable - ๐ก๏ธ Handle Never Types: Know that
never
disappears from unions - ๐จ Break Down Complex Logic: Use intermediate types for clarity
- โจ Test Edge Cases: Always test with
never
, single types, and complex unions - ๐ 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:
- ๐ป Practice with the form validation exercise above
- ๐๏ธ Build a distributed type system for your own project
- ๐ Move on to our next tutorial: Infer Keyword: Extracting Types in Conditionals
- ๐ 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! ๐๐ชโจ