3. Record — Build Typed Dictionaries
type Category = 'tools' | 'templates' | 'courses' | 'plugins'
interface CategoryMeta {
displayName: string
slug: string
productCount: number
}
// Keys MUST be every Category member — missing one is a compile error
const categoryRegistry: Record<Category, CategoryMeta> = {
tools: { displayName: 'Tools', slug: 'tools', productCount: 117 },
templates: { displayName: 'Templates', slug: 'templates', productCount: 843 },
courses: { displayName: 'Courses', slug: 'courses', productCount: 204 },
plugins: { displayName: 'Plugins', slug: 'plugins', productCount: 89 },
}
// Record with string keys for dynamic data
type FeatureFlags = Record<string, boolean>
const flags: FeatureFlags = { newCheckout: true, betaDashboard: false }
4. Partial and Required Together
interface Config {
apiUrl: string
timeout: number
retries: number
debug: boolean
logLevel: 'error' | 'warn' | 'info' | 'debug'
}
// All optional for user-provided overrides
type UserConfig = Partial<Config>
// Merge with defaults to get complete config
function resolveConfig(userConfig: UserConfig): Required<Config> {
const defaults: Required<Config> = {
apiUrl: 'https://api.wowhow.cloud',
timeout: 5000,
retries: 3,
debug: false,
logLevel: 'error',
}
return { ...defaults, ...userConfig }
}
// Required<T> strips undefined from all fields — useful for post-validation
interface RawFormInput {
name?: string
email?: string
message?: string
}
function validateForm(input: RawFormInput): Required<RawFormInput> {
if (!input.name || !input.email || !input.message) {
throw new Error('All fields required')
}
return input as Required<RawFormInput>
}
5. Extract and Exclude — Filter Union Types
type Status = 'draft' | 'published' | 'archived' | 'deleted' | 'pending_review'
// Extract: keep only matching members
type PublishableStatus = Extract<Status, 'draft' | 'pending_review'>
// Result: 'draft' | 'pending_review'
// Exclude: remove matching members
type ActiveStatus = Exclude<Status, 'archived' | 'deleted'>
// Result: 'draft' | 'published' | 'pending_review'
// Practical: exclude null/undefined from a type
type NonNullable<T> = Exclude<T, null | undefined> // this IS built-in, shown for clarity
type MaybeString = string | null | undefined
type DefinitelyString = NonNullable<MaybeString> // string
// Extract specific function overloads
type EventHandler = ((e: MouseEvent) => void) | ((e: KeyboardEvent) => void) | string
type FunctionHandlers = Extract<EventHandler, Function>
// Result: ((e: MouseEvent) => void) | ((e: KeyboardEvent) => void)
6. ReturnType — Infer a Function's Return Value
async function fetchUserWithOrders(userId: string) {
const user = await db.users.findUnique({ where: { id: userId } })
const orders = await db.orders.findMany({ where: { userId } })
return { user, orders, fetchedAt: new Date() }
}
// No need to manually define this type — infer it
type UserWithOrders = Awaited<ReturnType<typeof fetchUserWithOrders>>
// Useful when the return type is complex or changes frequently
function createApiResponse<T>(data: T, meta: { page: number; total: number }) {
return { data, meta, timestamp: Date.now() }
}
type ApiResponse<T> = ReturnType<typeof createApiResponse<T>>
// { data: T; meta: { page: number; total: number }; timestamp: number }
7. Parameters — Extract Function Arguments
function createOrder(
userId: string,
items: Array<{ productId: string; quantity: number }>,
couponCode?: string,
shippingAddress?: string
) {
// implementation
}
type CreateOrderArgs = Parameters<typeof createOrder>
// [userId: string, items: Array<...>, couponCode?: string, shippingAddress?: string]
// Extract individual parameter types
type OrderItems = CreateOrderArgs[1]
// Array<{ productId: string; quantity: number }>
// Useful for building test fixtures that stay in sync with function signatures
function mockCreateOrder(...args: CreateOrderArgs) {
const [userId, items, couponCode] = args
return { userId, items, couponCode, id: 'mock-id', status: 'pending' }
}
8. Readonly — Prevent Mutation
interface AppState {
user: { id: string; email: string }
cart: Array<{ productId: string; quantity: number }>
flags: Record<string, boolean>
}
// Deep readonly — mutation attempt is a compile error
type ImmutableState = Readonly<AppState>
// For deep readonly, use a recursive type
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
}
type FrozenState = DeepReadonly<AppState>
declare const state: FrozenState
// state.user.email = '[email protected]' // Error: Cannot assign to 'email' — it is read-only
9. ConstructorParameters — Infer Class Constructor Args
class ApiClient {
constructor(
private readonly baseUrl: string,
private readonly apiKey: string,
private readonly timeout = 5000
) {}
}
type ApiClientArgs = ConstructorParameters<typeof ApiClient>
// [baseUrl: string, apiKey: string, timeout?: number]
// Create factory without repeating the arg types
function createApiClient(...args: ApiClientArgs): ApiClient {
return new ApiClient(...args)
}
10. Mapped Types — Transform Every Key
interface UserProfile {
name: string
email: string
bio: string
website: string
}
// Make every field nullable
type NullableProfile = {
[K in keyof UserProfile]: UserProfile[K] | null
}
// Make every field an async getter
type AsyncProfile = {
[K in keyof UserProfile]: () => Promise<UserProfile[K]>
}
// Add validation state alongside each field
type FormField<T> = { value: T; error: string | null; touched: boolean }
type ProfileForm = {
[K in keyof UserProfile]: FormField<UserProfile[K]>
}
// Remap keys with 'as'
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type ProfileGetters = Getters<UserProfile>
// { getName: () => string; getEmail: () => string; ... }
11. Conditional Types — Branch on Type Shape
// IsArray: returns true if T is an array
type IsArray<T> = T extends unknown[] ? true : false
type A = IsArray<string[]> // true
type B = IsArray<string> // false
// UnwrapArray: get element type from array, passthrough if not
type UnwrapArray<T> = T extends (infer U)[] ? U : T
type C = UnwrapArray<string[]> // string
type D = UnwrapArray<number> // number
// UnwrapPromise: works with any depth
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T
type E = Awaited<Promise<Promise<string>>> // string
// Distribute over unions
type ToArray<T> = T extends unknown ? T[] : never
type StringOrNumberArrays = ToArray<string | number> // string[] | number[]
12. Template Literal Types — String Manipulation in the Type System
type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch'
type ApiEndpoint = '/users' | '/products' | '/orders'
// Combine to get all valid route identifiers
type RouteId = `${HttpMethod}:${ApiEndpoint}`
// 'get:/users' | 'get:/products' | 'post:/users' | ...
// CSS property names
type CssProperty = 'margin' | 'padding' | 'border'
type CssDirection = 'top' | 'right' | 'bottom' | 'left'
type CssDirectedProperty = `${CssProperty}-${CssDirection}`
// 'margin-top' | 'margin-right' | ... | 'border-left'
// Event handler naming convention
type EventName = 'click' | 'focus' | 'blur' | 'change'
type HandlerName = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur' | 'onChange'
13. Discriminated Unions — Safe State Machines
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error; retryCount: number }
function renderState<T>(state: FetchState<T>): string {
switch (state.status) {
case 'idle': return 'Ready to fetch'
case 'loading': return 'Loading...'
case 'success': return `Loaded ${JSON.stringify(state.data)}`
case 'error': return `Error: ${state.error.message} (retry ${state.retryCount})`
}
}
// TypeScript enforces exhaustive handling — add a new status and the switch breaks at compile time
14. Infer — Extract Types from Generic Constraints
// Extract the resolved type from a Promise
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T
// Extract the element type from an array
type ElementOf<T> = T extends Array<infer E> ? E : never
// Extract the key type from a Map
type MapKey<T> = T extends Map<infer K, unknown> ? K : never
type MapValue<T> = T extends Map<unknown, infer V> ? V : never
// Extract props from a React component
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never
// Real usage: derive event payload type from a handler
type HandlerPayload<T> = T extends (payload: infer P) => void ? P : never
type ClickHandler = (payload: { x: number; y: number; target: HTMLElement }) => void
type ClickPayload = HandlerPayload<ClickHandler>
// { x: number; y: number; target: HTMLElement }
15. Branded Types — Prevent ID Mixups
// Without branding, these are all just 'string' — easy to swap accidentally
declare function getOrder(orderId: string): Promise<Order>
declare function getUser(userId: string): Promise<User>
// With branding, mixing them is a compile error
declare const __brand: unique symbol
type Brand<T, B extends string> = T & { [__brand]: B }
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
type ProductId = Brand<string, 'ProductId'>
declare function getOrder(orderId: OrderId): Promise<Order>
declare function getUser(userId: UserId): Promise<User>
// Constructors validate and brand the IDs
function toUserId(raw: string): UserId {
if (!/^user_[a-z0-9]{20}$/.test(raw)) throw new Error('Invalid user ID')
return raw as UserId
}
// This now errors at compile time:
// getUser(orderId) // Argument of type 'OrderId' is not assignable to 'UserId'
People Also Ask
What is the difference between Pick and Extract in TypeScript?
Pick<T, K> works on object types — it creates a new type with only the keys K from object T. Extract<T, U> works on union types — it keeps only the union members assignable to U. Use Pick when you want a subset of an interface's properties; use Extract when you want a subset of a union's members.
When should I use a mapped type vs an interface?
Use interfaces when you know the exact keys at design time and want a named, documentable contract. Use mapped types when the keys are derived programmatically — from another type's keys, from a union, or from template literals. Mapped types are invaluable for keeping related types in sync: if UserProfile changes, ProfileForm and NullableProfile update automatically.
Are TypeScript utility types available at runtime?
No. All TypeScript types — including utility types — are erased at compile time. They exist only during type-checking and provide zero runtime overhead. If you need runtime validation (e.g., for API input), use a schema library like zod or valibot alongside your TypeScript types — they serve different purposes and complement each other.
Comments · 0
No comments yet. Be the first to share your thoughts.