Prerequisites
- Understanding of conditional types ๐ฏ
- Knowledge of generics ๐
- Familiarity with type patterns โก
What you'll learn
- Master the infer keyword syntax ๐๏ธ
- Extract types from complex structures ๐ง
- Build powerful type utilities ๐จ
- Debug type inference like a pro ๐
๐ฏ Introduction
Welcome to the world of the infer
keyword! ๐ Think of infer
as TypeScriptโs ultimate detective tool ๐ต๏ธโโ๏ธ - it can examine any type structure and extract exactly what you need, like finding a needle in a haystack โจ.
Youโre about to master one of TypeScriptโs most elegant and powerful features. Whether youโre building utility types ๐ ๏ธ, parsing complex APIs ๐, or creating type-safe abstractions ๐๏ธ, the infer
keyword will become your best friend.
By the end of this tutorial, youโll be extracting types with surgical precision and creating utilities that feel like magic! ๐ช Letโs embark on this detective journey! ๐
๐ Understanding Infer
๐ค What is the Infer Keyword?
Think of infer
as a type-level capture tool ๐ธ. Itโs like saying to TypeScript: โHey, when you match this pattern, please remember what type was in this specific spot so I can use it later!โ
The infer
keyword can only be used within conditional types, and it creates a type variable that captures part of the type being matched:
// ๐ฏ Basic infer pattern
type ExtractReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// ๐งช Let's see it in action!
type FunctionReturn1 = ExtractReturnType<() => string>; // string
type FunctionReturn2 = ExtractReturnType<(x: number) => boolean>; // boolean
type FunctionReturn3 = ExtractReturnType<string>; // never (not a function)
๐ก The Magic: infer R
says โcapture whatever the return type is and call it Rโ!
๐จ Infer in Action
Letโs see how infer works its magic:
// ๐ Extract array element type
type ArrayElement<T> = T extends (infer U)[] ? U : never;
// ๐งช Testing array element extraction
type StringElement = ArrayElement<string[]>; // string
type NumberElement = ArrayElement<number[]>; // number
type ObjectElement = ArrayElement<{id: string}[]>; // {id: string}
type NotArray = ArrayElement<string>; // never
// ๐ฎ Extract promise value type
type PromiseValue<T> = T extends Promise<infer U> ? U : never;
// โจ Testing promise extraction
type StringPromise = PromiseValue<Promise<string>>; // string
type UserPromise = PromiseValue<Promise<{name: string}>>; // {name: string}
type NotPromise = PromiseValue<string>; // never
๐ Key Insight: infer
creates a type variable that captures the matched part of the pattern!
๐ง Basic Infer Patterns
๐ Essential Infer Recipes
Letโs explore the most useful infer patterns:
// ๐จ Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// ๐งช Testing parameter extraction
type LoginParams = Parameters<(username: string, password: string) => boolean>;
// Result: [username: string, password: string]
type NoParams = Parameters<() => void>;
// Result: []
// ๐ Extract object property types
type PropertyType<T, K extends keyof T> = T extends { [key in K]: infer U } ? U : never;
// ๐ฎ Testing property extraction
interface User {
name: string;
age: number;
email: string;
}
type NameType = PropertyType<User, 'name'>; // string
type AgeType = PropertyType<User, 'age'>; // number
// โจ Extract first element of tuple
type Head<T> = T extends [infer H, ...any[]] ? H : never;
// ๐งช Testing head extraction
type FirstString = Head<[string, number, boolean]>; // string
type FirstNumber = Head<[number]>; // number
type EmptyHead = Head<[]>; // never
๐ฏ Advanced Infer Tricks
// ๐จ Extract last element of tuple
type Tail<T> = T extends [...any[], infer L] ? L : never;
// ๐งช Testing tail extraction
type LastBoolean = Tail<[string, number, boolean]>; // boolean
type LastOnly = Tail<[string]>; // string
type EmptyTail = Tail<[]>; // never
// ๐ Extract middle elements (without first and last)
type Middle<T> = T extends [any, ...infer M, any] ? M : never;
// ๐ฎ Testing middle extraction
type MiddleElements = Middle<[string, number, boolean, Date]>; // [number, boolean]
type TwoElements = Middle<[string, number]>; // []
type OneElement = Middle<[string]>; // never
// โจ Extract nested property type
type DeepPropertyType<T, K extends string> =
K extends `${infer Head}.${infer Tail}`
? T extends { [key in Head]: infer U }
? DeepPropertyType<U, Tail>
: never
: T extends { [key in K]: infer U }
? U
: never;
// ๐งช Testing deep property extraction
interface NestedUser {
profile: {
personal: {
name: string;
age: number;
};
settings: {
theme: "light" | "dark";
};
};
}
type DeepName = DeepPropertyType<NestedUser, "profile.personal.name">; // string
type DeepTheme = DeepPropertyType<NestedUser, "profile.settings.theme">; // "light" | "dark"
๐ก Practical Examples
๐ Example 1: Smart API Response Parser
Letโs build a type-safe API response parser using infer:
// ๐ฏ Define different API response patterns
interface ApiSuccess<T> {
status: "success";
data: T;
metadata?: {
timestamp: string;
requestId: string;
};
}
interface ApiError {
status: "error";
message: string;
code: number;
details?: string[];
}
interface ApiPaginated<T> {
status: "success";
data: T[];
pagination: {
page: number;
limit: number;
total: number;
hasMore: boolean;
};
}
// ๐ Smart type extractors using infer
type ExtractApiData<T> = T extends ApiSuccess<infer U>
? U
: T extends ApiPaginated<infer U>
? U[]
: never;
type ExtractApiError<T> = T extends ApiError
? { message: string; code: number; details?: string[] }
: never;
type ExtractPaginationInfo<T> = T extends ApiPaginated<any>
? T extends { pagination: infer P }
? P
: never
: never;
// ๐งช Testing our extractors
type UserApiResponse = ApiSuccess<{id: string; name: string; email: string}>;
type ProductListResponse = ApiPaginated<{id: string; title: string; price: number}>;
type ErrorResponse = ApiError;
type UserData = ExtractApiData<UserApiResponse>;
// Result: {id: string; name: string; email: string}
type ProductData = ExtractApiData<ProductListResponse>;
// Result: {id: string; title: string; price: number}[]
type ErrorInfo = ExtractApiError<ErrorResponse>;
// Result: { message: string; code: number; details?: string[] }
type PaginationInfo = ExtractPaginationInfo<ProductListResponse>;
// Result: { page: number; limit: number; total: number; hasMore: boolean }
// ๐ฎ Smart API response handler
class ApiResponseHandler {
// โจ Generic data extractor
extractData<T>(response: T): ExtractApiData<T> {
if (this.isSuccess(response)) {
return (response as any).data;
}
if (this.isPaginated(response)) {
return (response as any).data;
}
throw new Error("Cannot extract data from this response type ๐ข");
}
// ๐ฏ Type guards with infer-powered return types
isSuccess<T>(response: T): response is T extends ApiSuccess<any> ? T : never {
return (response as any).status === "success" &&
"data" in response &&
!("pagination" in response);
}
isPaginated<T>(response: T): response is T extends ApiPaginated<any> ? T : never {
return (response as any).status === "success" &&
"pagination" in response;
}
isError<T>(response: T): response is T extends ApiError ? T : never {
return (response as any).status === "error";
}
// ๐ Smart response processor
processResponse<T>(response: T): void {
if (this.isSuccess(response)) {
const data = this.extractData(response);
console.log("โ
Success data:", data);
} else if (this.isPaginated(response)) {
const data = this.extractData(response);
const pagination = response as any as ApiPaginated<any>;
console.log("๐ Paginated data:", data);
console.log("๐ Pagination:", pagination.pagination);
} else if (this.isError(response)) {
console.log("โ Error:", response.message);
console.log("๐ข Code:", response.code);
}
}
}
// ๐งช Usage example
const apiHandler = new ApiResponseHandler();
const userResponse: ApiSuccess<{id: string; name: string}> = {
status: "success",
data: { id: "user123", name: "Alice Wonder" },
metadata: { timestamp: "2023-12-01T10:30:00Z", requestId: "req-456" }
};
const productListResponse: ApiPaginated<{id: string; title: string}> = {
status: "success",
data: [
{ id: "prod1", title: "TypeScript Guide" },
{ id: "prod2", title: "JavaScript Mastery" }
],
pagination: { page: 1, limit: 10, total: 25, hasMore: true }
};
// ๐ฏ Process with full type safety
apiHandler.processResponse(userResponse);
apiHandler.processResponse(productListResponse);
๐ฎ Example 2: Redux Action Type Extractor
Letโs create a type-safe Redux pattern using infer:
// ๐ Define action patterns
interface BaseAction {
type: string;
}
interface PayloadAction<T = any> extends BaseAction {
payload: T;
}
interface MetaAction<T = any, M = any> extends PayloadAction<T> {
meta: M;
}
// ๐ฏ Action creators with different patterns
const createUser = (userData: {name: string; email: string}) => ({
type: "CREATE_USER" as const,
payload: userData
});
const deleteUser = (userId: string) => ({
type: "DELETE_USER" as const,
payload: { userId }
});
const updateUserWithMeta = (userId: string, updates: Partial<{name: string; email: string}>) => ({
type: "UPDATE_USER" as const,
payload: { userId, updates },
meta: { timestamp: Date.now(), source: "user-action" }
});
const resetState = () => ({
type: "RESET_STATE" as const
});
// ๐ Infer-powered type extractors
type ExtractActionType<T> = T extends { type: infer U } ? U : never;
type ExtractPayload<T> = T extends { payload: infer P } ? P : never;
type ExtractMeta<T> = T extends { meta: infer M } ? M : never;
type ExtractActionFromCreator<T> = T extends (...args: any[]) => infer A ? A : never;
// ๐งช Testing our extractors
type CreateUserAction = ExtractActionFromCreator<typeof createUser>;
// Result: { type: "CREATE_USER"; payload: {name: string; email: string} }
type DeleteUserAction = ExtractActionFromCreator<typeof deleteUser>;
// Result: { type: "DELETE_USER"; payload: { userId: string } }
type UpdateUserAction = ExtractActionFromCreator<typeof updateUserWithMeta>;
// Result: { type: "UPDATE_USER"; payload: {...}; meta: {...} }
type ResetAction = ExtractActionFromCreator<typeof resetState>;
// Result: { type: "RESET_STATE" }
// โจ Extract specific parts
type CreateUserType = ExtractActionType<CreateUserAction>; // "CREATE_USER"
type CreateUserPayload = ExtractPayload<CreateUserAction>; // {name: string; email: string}
type UpdateUserMeta = ExtractMeta<UpdateUserAction>; // {timestamp: number; source: string}
// ๐จ Create union of all action types
type AllActionCreators =
| typeof createUser
| typeof deleteUser
| typeof updateUserWithMeta
| typeof resetState;
type AllActions = ExtractActionFromCreator<AllActionCreators>;
// ๐ฎ Type-safe reducer using infer
type ReducerFunction<S, A> = (state: S, action: A) => S;
interface UserState {
users: Array<{id: string; name: string; email: string}>;
loading: boolean;
lastUpdated?: number;
}
// ๐ Smart reducer with infer-powered action handling
const userReducer: ReducerFunction<UserState, AllActions> = (state, action) => {
// TypeScript knows exactly what action.type and action.payload can be!
switch (action.type) {
case "CREATE_USER":
return {
...state,
users: [...state.users, {
id: `user-${Date.now()}`,
...action.payload // TypeScript knows this is {name: string; email: string}
}],
loading: false
};
case "DELETE_USER":
return {
...state,
users: state.users.filter(user =>
user.id !== action.payload.userId // TypeScript knows payload has userId
),
loading: false
};
case "UPDATE_USER":
return {
...state,
users: state.users.map(user =>
user.id === action.payload.userId
? { ...user, ...action.payload.updates }
: user
),
lastUpdated: action.meta.timestamp, // TypeScript knows meta exists!
loading: false
};
case "RESET_STATE":
return {
users: [],
loading: false
};
default:
return state;
}
};
// ๐งช Usage with full type safety
const initialState: UserState = { users: [], loading: false };
// All actions are type-safe!
const action1 = createUser({ name: "Alice", email: "[email protected]" });
const action2 = deleteUser("user123");
const action3 = updateUserWithMeta("user123", { name: "Alice Wonder" });
const newState1 = userReducer(initialState, action1);
const newState2 = userReducer(newState1, action2);
const newState3 = userReducer(newState2, action3);
console.log("๐ฏ Final state:", newState3);
๐ Advanced Infer Techniques
๐งโโ๏ธ Multiple Infer Variables
You can use multiple infer
variables in the same conditional type:
// ๐จ Extract both key and value types from object entries
type ObjectEntry<T> = T extends { [K in infer Key]: infer Value }
? { key: Key; value: Value }
: never;
// ๐งช Testing multiple infer
interface ProductCatalog {
electronics: { laptops: number; phones: number };
books: { fiction: number; technical: number };
clothing: { shirts: number; pants: number };
}
type CatalogEntry = ObjectEntry<ProductCatalog>;
// Result: { key: "electronics" | "books" | "clothing"; value: object }
// ๐ Extract function signature components
type FunctionSignature<T> = T extends (
...args: infer Args
) => infer Return
? { parameters: Args; returnType: Return }
: never;
// ๐ฎ Testing signature extraction
type LoginSignature = FunctionSignature<(username: string, password: string) => Promise<boolean>>;
// Result: { parameters: [username: string, password: string]; returnType: Promise<boolean> }
๐๏ธ Recursive Infer Patterns
// ๐ฏ Deep flatten array types
type DeepFlatten<T> = T extends (infer U)[]
? U extends any[]
? DeepFlatten<U>
: U
: T;
// ๐งช Testing deep flatten
type NestedArray = string[][][];
type FlattenedArray = DeepFlatten<NestedArray>; // string
// โจ Extract all property names recursively
type DeepKeys<T> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? `${K}` | `${K}.${DeepKeys<T[K]>}`
: `${K}`
: never;
}[keyof T]
: never;
// ๐ Testing deep keys
interface NestedConfig {
api: {
endpoints: {
users: string;
products: string;
};
timeout: number;
};
ui: {
theme: string;
language: string;
};
}
type AllConfigKeys = DeepKeys<NestedConfig>;
// Result: "api" | "api.endpoints" | "api.endpoints.users" | "api.endpoints.products" | "api.timeout" | "ui" | "ui.theme" | "ui.language"
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Infer in Wrong Context
// โ Can't use infer outside conditional types!
type BadInfer<T> = infer U; // โ Error!
// โ
Infer only works in conditional type expressions
type GoodInfer<T> = T extends Array<infer U> ? U : never;
๐คฏ Pitfall 2: Multiple Infer Same Variable
// โ Using same infer variable name can be confusing
type ConfusingInfer<T> = T extends {
a: infer U;
b: infer U; // Same U - will be intersection type!
} ? U : never;
// ๐งช Testing confusing infer
type TestConfusing = ConfusingInfer<{ a: string; b: number }>; // string & number (never!)
// โ
Use different variable names for clarity
type ClearInfer<T> = T extends {
a: infer A;
b: infer B;
} ? { propA: A; propB: B } : never;
type TestClear = ClearInfer<{ a: string; b: number }>; // { propA: string; propB: B }
๐ Pitfall 3: Infer with Distribution
// โ Distribution can affect infer behavior
type DistributedInfer<T> = T extends Array<infer U> ? U : never;
// ๐งช Testing with union
type UnionArrays = string[] | number[];
type DistributedResult = DistributedInfer<UnionArrays>; // string | number (distributed!)
// โ
Prevent distribution if needed
type NonDistributedInfer<T> = [T] extends [Array<infer U>] ? U : never;
type NonDistributedResult = NonDistributedInfer<UnionArrays>; // never (no distribution)
๐ ๏ธ Best Practices
- ๐ฏ Use Descriptive Infer Names:
infer ElementType
is better thaninfer U
- ๐ Start Simple: Master basic patterns before attempting complex recursion
- ๐ก๏ธ Handle Edge Cases: Always consider what happens with
never
andunknown
- ๐จ Test Your Patterns: Use type tests to verify your infer logic works correctly
- โจ Combine with Distribution: Understand how infer interacts with union types
- ๐ Debug Step by Step: Break complex infer patterns into smaller, testable pieces
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Smart Database Query Type System
Create an infer-powered type system for type-safe database queries:
๐ Requirements:
- โ Extract table names from query strings
- ๐ง Parse SELECT fields and infer result types
- ๐จ Handle WHERE clauses with type safety
- ๐ Support JOIN operations
- โจ Type-safe query builder!
๐ Bonus Points:
- Add INSERT/UPDATE/DELETE query parsing
- Create nested query support
- Build query validation system
๐ก Solution
๐ Click to see solution
// ๐ฏ Database schema definition
interface DatabaseSchema {
users: {
id: number;
name: string;
email: string;
age: number;
active: boolean;
};
products: {
id: number;
title: string;
price: number;
category: string;
inStock: boolean;
};
orders: {
id: number;
userId: number;
productId: number;
quantity: number;
total: number;
};
}
// ๐ Query parsing with infer
type ParseSelectQuery<Q extends string> =
Q extends `SELECT ${infer Fields} FROM ${infer Table}`
? {
operation: "SELECT";
fields: ParseFields<Fields>;
table: Table extends keyof DatabaseSchema ? Table : never;
resultType: Table extends keyof DatabaseSchema
? ProjectFields<DatabaseSchema[Table], ParseFields<Fields>>
: never;
}
: never;
// ๐จ Parse field list
type ParseFields<F extends string> =
F extends "*"
? "*"
: F extends `${infer Field}, ${infer Rest}`
? [Field, ...ParseFields<Rest>]
: [F];
// โจ Project specific fields from table schema
type ProjectFields<Schema, Fields> =
Fields extends "*"
? Schema
: Fields extends readonly (infer F)[]
? F extends keyof Schema
? Pick<Schema, F>
: never
: never;
// ๐งช Advanced query parsing with WHERE clauses
type ParseQueryWithWhere<Q extends string> =
Q extends `${infer BaseQuery} WHERE ${infer WhereClause}`
? ParseSelectQuery<BaseQuery> extends infer BaseResult
? BaseResult extends { operation: "SELECT"; table: infer Table }
? BaseResult & {
where: ParseWhereClause<WhereClause>;
filters: Table extends keyof DatabaseSchema
? ExtractWhereFields<WhereClause, DatabaseSchema[Table]>
: never;
}
: never
: never
: ParseSelectQuery<Q>;
// ๐ฏ Parse WHERE clause conditions
type ParseWhereClause<W extends string> =
W extends `${infer Field} = ${infer Value}`
? { field: Field; operator: "="; value: Value }
: W extends `${infer Field} > ${infer Value}`
? { field: Field; operator: ">"; value: Value }
: W extends `${infer Field} < ${infer Value}`
? { field: Field; operator: "<"; value: Value }
: { unparsed: W };
// ๐ Extract and validate WHERE fields
type ExtractWhereFields<W extends string, Schema> =
W extends `${infer Field} = ${infer Value}`
? Field extends keyof Schema
? { [K in Field]: Schema[Field] }
: never
: {};
// ๐ฎ Type-safe query builder
class TypeSafeQueryBuilder<Schema extends DatabaseSchema = DatabaseSchema> {
// โจ Execute SELECT query with full type safety
select<Q extends string>(
query: Q
): ParseQueryWithWhere<Q> extends { resultType: infer R } ? R[] : never {
// Mock implementation - in real app, this would execute the actual query
console.log(`๐ Executing query: ${query}`);
// Parse and validate query at runtime
const parsed = this.parseQuery(query);
console.log(`๐ Parsed query:`, parsed);
// Return mock results with correct typing
return [] as any;
}
// ๐ฏ Parse query at runtime (implementation detail)
private parseQuery(query: string) {
// Simple parser for demonstration
const selectMatch = query.match(/SELECT (.+) FROM (\w+)/i);
if (selectMatch) {
const [, fields, table] = selectMatch;
return {
operation: "SELECT",
fields: fields === "*" ? "*" : fields.split(",").map(f => f.trim()),
table: table.toLowerCase()
};
}
return null;
}
// ๐ Fluent interface with type safety
from<T extends keyof Schema>(table: T) {
return new TableQueryBuilder<Schema, T>(table);
}
}
// ๐จ Table-specific query builder
class TableQueryBuilder<Schema extends DatabaseSchema, Table extends keyof Schema> {
constructor(private table: Table) {}
// โจ Type-safe field selection
select<Fields extends keyof Schema[Table]>(
...fields: Fields[]
): Pick<Schema[Table], Fields>[] {
console.log(`๐ SELECT ${fields.join(", ")} FROM ${String(this.table)}`);
return [] as Pick<Schema[Table], Fields>[];
}
// ๐ฏ Type-safe WHERE conditions
where<Field extends keyof Schema[Table]>(
field: Field,
operator: "=" | ">" | "<" | ">=",
value: Schema[Table][Field]
): this {
console.log(`๐ WHERE ${String(field)} ${operator} ${value}`);
return this;
}
// ๐ Get all records with full typing
all(): Schema[Table][] {
console.log(`๐ฎ SELECT * FROM ${String(this.table)}`);
return [] as Schema[Table][];
}
}
// ๐งช Usage examples with full type safety
const queryBuilder = new TypeSafeQueryBuilder<DatabaseSchema>();
// โ
Type-safe string queries
const userResults = queryBuilder.select("SELECT name, email FROM users");
// Type: { name: string; email: string }[]
const productResults = queryBuilder.select("SELECT * FROM products");
// Type: { id: number; title: string; price: number; category: string; inStock: boolean }[]
const queryWithWhere = queryBuilder.select("SELECT title, price FROM products WHERE category = 'electronics'");
// Type: { title: string; price: number }[]
// ๐ฏ Fluent interface with type safety
const users = queryBuilder
.from("users")
.select("name", "email", "age") // TypeScript ensures these fields exist!
.where("active", "=", true) // TypeScript ensures 'active' is boolean!
const products = queryBuilder
.from("products")
.where("price", ">", 50) // TypeScript ensures 'price' is number!
.where("inStock", "=", true)
.all();
// โ These would cause TypeScript errors:
// queryBuilder.select("SELECT invalid_field FROM users"); // invalid_field doesn't exist
// queryBuilder.from("users").where("age", "=", "25"); // age should be number, not string
// queryBuilder.from("invalid_table"); // table doesn't exist
console.log("๐ All queries executed with type safety!");
๐ Key Takeaways
Youโve mastered the infer
keyword! Hereโs what you can now do:
- โ Extract types from complex structures with surgical precision ๐ช
- โ Build powerful utility types that adapt automatically ๐ก๏ธ
- โ Parse and transform type patterns like a detective ๐ฏ
- โ Debug type inference issues with confidence ๐
- โ Create type-safe APIs that feel magical ๐
Remember: The infer
keyword is like having X-ray vision ๐ for types - it can see inside any type structure and extract exactly what you need!
๐ค Next Steps
Congratulations! ๐ Youโve conquered the infer
keyword!
Hereโs what to explore next:
- ๐ป Practice with the database query exercise above
- ๐๏ธ Build your own infer-powered utility types
- ๐ Explore combining
infer
with mapped types and template literals - ๐ Share your infer magic with the TypeScript community!
You now possess one of TypeScriptโs most powerful type-level tools. Use it to create utilities that feel like magic and APIs that are impossible to use incorrectly. Remember - every TypeScript wizard started with curiosity. Keep experimenting, keep learning, and most importantly, have fun with types! ๐โจ
Happy type detective work! ๐๐โจ