Learn to create powerful type utilities using TypeScript's conditional types and infer keyword, including UnwrapPromise, ReturnType, Parameters, and array element extraction.
// ============================================
// Conditional Types and Infer in TypeScript
// ============================================
// 1. UnwrapPromise<T> - Extract the resolved type from a Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// Examples
type PromiseString = UnwrapPromise<Promise<string>>; // string
type PromiseNumber = UnwrapPromise<Promise<number>>; // number
type NotAPromise = UnwrapPromise<string>; // string (passthrough)
type NestedPromise = UnwrapPromise<Promise<Promise<boolean>>>; // Promise<boolean>
// Deep unwrap variant
type DeepUnwrapPromise<T> = T extends Promise<infer U> ? DeepUnwrapPromise<U> : T;
type DeeplyNested = DeepUnwrapPromise<Promise<Promise<Promise<string>>>>; // string
// 2. Custom ReturnType<T> - Extract function return type
type MyReturnType<T extends (...args: unknown[]) => unknown> =
T extends (...args: unknown[]) => infer R ? R : never;
// Examples
const fetchUser = async (id: number): Promise<{ name: string; age: number }> => {
return { name: "Alice", age: 30 };
};
type FetchUserReturn = MyReturnType<typeof fetchUser>; // Promise<{ name: string; age: number }>
type SyncReturn = MyReturnType<() => string>; // string
// 3. Custom Parameters<T> - Extract function parameter types as tuple
type MyParameters<T extends (...args: unknown[]) => unknown> =
T extends (...args: infer P) => unknown ? P : never;
// Examples
const greet = (name: string, age: number, active: boolean): string => {
return `Hello ${name}, you are ${age} years old`;
};
type GreetParams = MyParameters<typeof greet>; // [string, number, boolean]
type FirstParam = MyParameters<typeof greet>[0]; // string
// 4. UnwrapArray<T> / ElementType<T> - Extract array element type
type UnwrapArray<T> = T extends Array<infer U> ? U : T;
type ElementType<T> = T extends (infer U)[] ? U : T;
// Examples
type StringArrayElement = UnwrapArray<string[]>; // string
type NumberArrayElement = ElementType<number[]>; // number
type TupleElement = UnwrapArray<[string, number, boolean]>; // string | number | boolean
type NotArray = UnwrapArray<string>; // string (passthrough)
// 5. Distributive Conditional Types
// When T is a union, conditional types distribute over each member
type ToArray<T> = T extends unknown ? T[] : never;
// Distributive behavior: each union member is processed separately
type Distributed = ToArray<string | number>; // string[] | number[]
// Prevent distribution with tuple wrapper
type ToArrayNonDistributive<T> = [T] extends [unknown] ? T[] : never;
type NonDistributed = ToArrayNonDistributive<string | number>; // (string | number)[]
// 6. Practical Use Cases
// API Response Unwrapper
type ApiResponse<T> = Promise<{ data: T; status: number }>;
type ExtractData<T> = T extends Promise<{ data: infer D; status: number }> ? D : never;
type UserResponse = ApiResponse<{ id: number; email: string }>;
type UserData = ExtractData<UserResponse>; // { id: number; email: string }
// Event Handler Type Extraction
type EventMap = {
click: { x: number; y: number };
keypress: { key: string; code: number };
scroll: { scrollTop: number };
};
type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void;
type ExtractEventPayload<T> = T extends (event: infer E) => void ? E : never;
type ClickHandler = EventHandler<"click">;
type ClickPayload = ExtractEventPayload<ClickHandler>; // { x: number; y: number }
// Constructor Parameter Extraction
type ConstructorParameters<T extends abstract new (...args: unknown[]) => unknown> =
T extends abstract new (...args: infer P) => unknown ? P : never;
class Database {
constructor(public host: string, public port: number, public ssl: boolean) {}
}
type DbParams = ConstructorParameters<typeof Database>; // [string, number, boolean]
// 7. Advanced: Infer in Template Literal Types
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type RouteParams = ExtractRouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId"
// 8. Combining Multiple Infers
type FunctionInfo<T extends (...args: unknown[]) => unknown> =
T extends (...args: infer A) => infer R
? { args: A; return: R; argCount: A["length"] }
: never;
type GreetInfo = FunctionInfo<typeof greet>;
// { args: [string, number, boolean]; return: string; argCount: 3 }
// 9. Runtime Usage Example
function createTypedFetch<T extends (...args: unknown[]) => Promise<unknown>>(
fetcher: T
): (...args: MyParameters<T>) => MyReturnType<T> {
return fetcher as (...args: MyParameters<T>) => MyReturnType<T>;
}
const typedFetchUser = createTypedFetch(fetchUser);
const result = typedFetchUser(123); // Promise<{ name: string; age: number }>
// 10. Conditional Type Constraints with infer
type GetPropertyType<T, K extends keyof T> =
T[K] extends infer V ? V : never;
interface User {
id: number;
name: string;
metadata: { lastLogin: Date };
}
type UserNameType = GetPropertyType<User, "name">; // string
type MetadataType = GetPropertyType<User, "metadata">; // { lastLogin: Date }
console.log("All type utilities compiled successfully!");Conditional types with infer are one of TypeScript's most powerful features for creating flexible, reusable type utilities. The infer keyword allows you to declare a type variable within a conditional type's extends clause, effectively 'capturing' a type from a complex type structure. This pattern is the foundation for many built-in utility types like ReturnType, Parameters, and Awaited.
The core syntax follows the pattern T extends SomeType<infer U> ? U : Fallback. When TypeScript evaluates this, it checks if T matches the structure SomeType<something>, and if so, it captures that 'something' as U. This is incredibly useful for unwrapping generic types—extracting the inner type from Promise, Array, or function signatures without knowing what that inner type is ahead of time.
Distributive conditional types are a crucial behavior to understand. When a conditional type acts on a naked type parameter (like T extends U ? X : Y), and that type parameter receives a union type, TypeScript automatically distributes the conditional across each union member. This means ToArray<string | number> becomes ToArray<string> | ToArray<number>, resulting in string[] | number[] rather than (string | number)[]. You can prevent this distribution by wrapping the type parameter in a tuple: [T] extends [U] ? X : Y.
The practical applications shown here include API response unwrapping, event handler type extraction, and route parameter parsing. These patterns are essential in real-world codebases—particularly when building type-safe wrappers around APIs, creating form libraries, or building routing systems. The template literal type example demonstrates how infer can even extract substrings from literal types, enabling compile-time parsing of URL patterns.
When using conditional types with infer, be mindful of complexity. Deeply nested conditional types can become difficult to read and may impact TypeScript's compilation performance. For simple cases, prefer direct property access or mapped types. Reserve conditional types with infer for situations where you genuinely need to extract types from complex structures or when building reusable utility types that must handle various input shapes.