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.

Code Farmer Manor Chronicle
Code Farmer Manor Chronicle
Code Farmer Manor Chronicle
Master TypeScript Generics & Utility Types: From Basics to Production-Ready Patterns

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: string

Generic 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 error

Pitfall: 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 length

Common pattern: withId<T extends { id: number }>(item) requires id while preserving other fields.

keyof T and Index Access Types

keyof T

produces 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 error

ReturnType<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>  // number

Note: 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 errors

Common 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."

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptGenericsType SafetyUtility TypesConditional TypesInferKeyofIndex Access Types
Code Farmer Manor Chronicle
Written by

Code Farmer Manor Chronicle

A heart like drifting clouds, ever at ease; a mind like flowing water, free to roam.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.