Master TypeScript Generics & Utility Types: From Basics to Production-Ready Patterns
This practical guide covers TypeScript generics, constraints, keyof, index access types, nine essential utility types (Partial, Pick, Omit, Record, ReturnType, etc.), conditional types with infer, and distributive conditional types, demonstrating real-world patterns for API responses, form handling, and permission mapping with executable code examples.
Generics: Type-Level Parameters
Generics let types act like parameters, producing precise types instead of repeating code or using any which disables checking.
Basic Generic Function
function getFirst<T>(arr: T[]): T {
return arr[0]
}
const a = getFirst([1, 2, 3]) // a: number (T inferred as number)
const b = getFirst(['x', 'y']) // b: stringGeneric Interface for API Responses
interface ApiResponse<T> {
code: number
message: string
data: T
}
interface User { id: number; name: string }
const res: ApiResponse<User[]> = await fetchUserList()
res.data[0].name // ✅ typed
res.data[0].nickname // ❌ compile errorPitfall: Generic naming is convention only; if the function body never uses T, the generic is a placeholder.
Generic Constraints with extends
Without constraints, T is unknown. extends requires a minimum shape:
// Without constraint: error (T may lack length)
// function logLen<T>(x: T) { console.log(x.length) } // ❌
function logLen<T extends { length: number }>(x: T): number {
return x.length // ✅ constraint guarantees length exists
}
logLen('abc') // ✅
logLen([1, 2]) // ✅
logLen(123) // ❌ number has no lengthCommon pattern: withId<T extends { id: number }>(item) requires id while preserving other fields.
keyof T and Index Access Types
keyof Tproduces a union of an object's keys. Combined with generics, it enables safe property access:
interface User { id: number; name: string; age: number }
type UserKeys = keyof User // 'id' | 'name' | 'age'
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
getProp(user, 'name') // ✅ string
getProp(user, 'phone') // ❌ compile error T[K]is called an index access type — the type of property K on T.
Nine Essential Built-in Utility Types
All examples based on User with fields: id, name, email?, createdAt, password.
Partial<T> — makes all properties optional. Use case: edit forms where only a subset is filled initially.
let editForm: Partial<User> = { name: '张三' }Required<T> — makes all properties required. Opposite of Partial.
Readonly<T> — makes all properties readonly. Use case: prevent mutation of config objects.
Pick<T, K> — selects a subset of keys. Pick<User, 'id' | 'name'> for list views. Safety: only real keys allowed; renaming a field in User triggers compile errors in Pick.
Omit<T, K> — removes keys. Omit<User, 'password'> for safe external responses.
Record<K, V> — maps keys to a uniform value type. Ensures exhaustive mapping for enums/unions:
type Theme = 'light' | 'dark'
const themeLabel: Record<Theme, string> = {
light: '浅色',
dark: '深色'
} // missing a key causes compile errorReturnType<F> — extracts a function's return type. type UserResult = ReturnType<typeof createUser> avoids manual duplication.
Parameters<F> — extracts a function's parameter types as a tuple. Parameters<typeof save>[0] gets the first argument type.
NonNullable<T> — removes null and undefined from a union. Use case: sanitize API responses.
Conditional Types and infer : Types That Decide
Conditional Types: Ternary at Type Level
type IsString<T> = T extends string ? 'yes' : 'no'
type A = IsString<'x'> // 'yes'
type B = IsString<123> // 'no'infer : Extracting Types During Matching
Like destructuring, infer captures inner types from complex structures:
// Unwrap Promise
type Awaited<T> = T extends Promise<infer V> ? V : T
type R1 = Awaited<Promise<number>> // number
// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : T
type R3 = ElementType<(1 | 2)[]> // 1 | 2
// Vue3 Ref unwrapping
type UnwrapRef<T> = T extends { value: infer V } ? V : T
type Count = UnwrapRef<typeof count> // numberNote: Conditional types on unresolved generics show deferred evaluation (long type displays) — this is normal and doesn't affect correctness.
Distributive Conditional Types
When T is a union, conditional types distribute over each member:
type ToArray<T> = T extends any ? T[] : never
type R = ToArray<string | number> // string[] | number[]
// Prevent distribution by wrapping in a tuple
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type R2 = ToArrayNonDist<string | number> // (string | number)[]Real-World Composition: Chaining Utility Types
Form Scenario Trio
interface UserEntity {
id: number; name: string; email: string; password: string; status: 0 | 1
}
// Create form: no id, password required
type UserCreateForm = Omit<UserEntity, 'id' | 'status'>
// Update form: no id; password optional
type UserUpdateForm = Partial<Omit<UserEntity, 'id' | 'status'>>
// List response: strip sensitive field
type UserVO = Omit<UserEntity, 'password'>Generic API Wrapper
interface PageResult<T> { records: T[]; total: number }
export function useApi<T>(url: string) {
return {
getById: (id: number) => request.get<any, T>(`${url}/${id}`),
page: (params: Partial<T> & { current?: number; size?: number }) =>
request.get<any, PageResult<T>>(url, { params }),
create: (data: Omit<T, 'id'>) => request.post<any, T>(url, data),
remove: (id: number) => request.delete<any, void>(`${url}/${id}`)
}
}
const userApi = useApi<UserVO>('/api/user')Note: request.get<any, T> follows axios convention where the second type parameter is the response data type.
Permission Mapping with Exhaustiveness
type PermCode = 'user:add' | 'user:edit' | 'user:delete'
const permLabelMap: Record<PermCode, string> = {
'user:add': '新增用户',
'user:edit': '编辑用户',
'user:delete': '删除用户'
} // missing or extra keys cause compile errorsCommon Pitfalls Quick Reference
Generic passed but unused in function body — generic must appear in parameters or return type; otherwise it's decorative.
Property access on T errors — add constraint T extends { xxx } or use keyof indexing.
Pick / Omit key shows red — only real keys allowed; this prevents typos and syncs with source changes.
typeof obj infers too wide — use as const to lock literals before typeof.
Cheat Sheet Summary
Parameterize types: <T>(x: T) => T Constrain capabilities: T extends { length: number } Union of keys: keyof T All optional / required / readonly: Partial / Required / Readonly Select / remove fields: Pick<T, K> / Omit<T, K> Dictionary with exhaustiveness: Record<K, V> Copy function return type: ReturnType<typeof fn> Extract from wrapper: T extends Promise<infer V> ? V : T Takeaway: Generics and utility types aren't clever tricks — they turn types from annotations into computation tools. When interfaces change, fields are renamed, or permissions grow, the compiler catches errors instead of production users. That's the real value of "types checking for you."
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Farmer Manor Chronicle
A heart like drifting clouds, ever at ease; a mind like flowing water, free to roam.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
