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
- ๐ฏ Be Explicit: Always handle the false case clearly
- ๐ Use Meaningful Names:
IsArray<T>
is better thanCheck<T>
- ๐ก๏ธ Prevent Infinite Recursion: Set depth limits for recursive types
- ๐จ Leverage Distribution: Understand when unions distribute and when they donโt
- โจ Keep It Simple: Donโt over-engineer your conditional types
- ๐ 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:
- ๐ป Practice with the exercise above - try different API patterns
- ๐๏ธ Build a library using advanced conditional types
- ๐ Move on to our next tutorial: Distributed Conditional Types: Advanced Patterns
- ๐ 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! ๐๐ฎโจ