TypeScript style and type safety patterns. Applies to TSX, TS code, interfaces, types, generics, unions, type errors.
| Pattern | Do | Don't |
|---|---|---|
| Object shapes | interface |
type |
| Composition | interface extends |
type & |
| Unions | discriminated, <10 members | bag of optionals |
| Constants | as const objects |
enum |
| Imports | import type { T } |
import { type T } |
| Return types | explicit on exports | infer (except JSX) |
| Validation | satisfies |
manual annotation |
| Data | readonly default |
mutable default |
| Mutation | spread operator | direct mutation |
| Async | Promise.all when independent |
sequential awaits |
For Convex: see convex-patterns/SKILL.md.
// ✅
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error }
// ❌ allows impossible states
type AsyncState<T> = { status: string; data?: T; error?: Error }
// ✅ validates + infers literal types
return { type, severity, description } satisfies Partial<FraudFlag>
// ❌ loses inference
function createConfig(): Partial<Config> { return { timeout: 5000 } }
// ✅ forces acknowledgment
interface CreateUser { referrerId: string | undefined }
// ❌ silent omission possible
interface CreateUser { referrerId?: string }
// ✅ cached, flat
interface ButtonProps extends BaseProps, InteractiveProps {
variant: "primary" | "secondary"
}
// ❌ recursive merge, slow
type ButtonProps = BaseProps & InteractiveProps & { variant: "primary" | "secondary" }
Use & only for: type A = (B | C) & D
// ✅ tree-shakeable, no runtime
const Status = { Pending: "pending", Active: "active" } as const
type Status = (typeof Status)[keyof typeof Status]
// ❌ generates runtime code
enum Status { Pending = "pending" }
// ✅ spread
const updated = { ...user, name: "New" }
const added = [...items, newItem]
// ❌ mutation
user.name = "New"
items.push(newItem)
// ✅ parallel
const [users, markets] = await Promise.all([fetchUsers(), fetchMarkets()])
// ❌ sequential (when independent)
const users = await fetchUsers()
const markets = await fetchMarkets()
// ❌ O(n²)
type Status = "pending" | "processing" | "confirmed" | "shipped" | ...
// ✅ nested
type Status =
| { category: "active"; state: "pending" | "processing" }
| { category: "completed"; state: "delivered" | "shipped" }
switch (state.status) {
case "success": return ...
default: {
const _exhaustive: never = state
throw new Error("Unhandled state")
}
}
// ❌ recalculated
interface Api<T> { fetch<U>(x: U): U extends TypeA<T> ? ProcessA<U, T> : U }
// ✅ cached
type FetchResult<U, T> = U extends TypeA<T> ? ProcessA<U, T> : U
interface Api<T> { fetch<U>(x: U): FetchResult<U, T> }
type Result = Success | Errortype Readonly<T> = { readonly [K in keyof T]: T[K] }type Unwrap<T> = T extends Promise<infer U> ? U : Ttype Pair = [string, number]type User = Infer<typeof userValidator>