Production-ready TypeScript best practices — tsconfig, type patterns, generics, error handling, and code organization for strict, maintainable codebases
npx skills add m10rten/typescript-bits --skill typescript-best-practicesskill.md · 202 lines~1.7kPractical, production-ready TypeScript patterns for strict, maintainable codebases. Apply these when writing or reviewing TypeScript code.
Enable all strict checks — these catch real bugs at compile time:
{
"compilerOptions": {
// Enables all strict type-checking (noImplictAny, strictNullChecks, etc.)
"strict": true,
// Forces handling `undefined` from bracket access on arrays/objects
"noUncheckedIndexedAccess": true,
// Prevents unused variables from compiling
"noUnusedLocals": true,
// Prevents unused function parameters from compiling
"noUnusedParameters": true,
// Optional properties can't be explicitly set to `undefined`
"exactOptionalPropertyTypes": true,
// Enforces `import type` / `export type` — keeps runtime bundles clean
"verbatimModuleSyntax": true
}
}`type` unions instead of `enum` — they're erasable at runtime, tree-shakeable, and compose naturally.`const` objects with `as const` when you need both runtime values and type-level unions.// Prefer this:
type Status = "active" | "inactive" | "pending";
// Over this:
enum Status {
Active = "active",
Inactive = "inactive",
Pending = "pending",
}Use branded types to distinguish structurally identical primitives (e.g., two `string` IDs that must not be confused):
type UserId = string & { __brand: "UserId" };
type PostId = string & { __brand: "PostId" };Model distinct states as discriminated unions with a literal `type` or `kind` discriminant. This gives you exhaustiveness checking and narrows each branch automatically:
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };Switch on the discriminant for narrowing & exhaustiveness:
type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };
function area(s: Shape) {
switch (s.kind) {
case "circle":
return Math.PI * s.radius ** 2;
case "square":
return s.side ** 2;
}
}| Type | Use case |
|---|---|
`Pick | Subset of known keys (stable API slice) |
`Omit | Exclude keys (e.g., strip internal fields) |
`Partial | Gradual construction / update payloads |
`Required | After validation — mark all fields as required |
`Readonly | Config objects, constants passed to consumers |
`Record | Dynamic key-value maps (use with `noUncheckedIndexedAccess`) |
Use `infer` to extract unwrapped types:
type Unwrap<T> = T extends Promise<infer U> ? U : T;`interface` for Public APIs, `type` for Computed Types`interface` for object shapes that consumers implement or extend (declaration merging, better error messages).`type` for unions, intersections, mapped/conditional types, tuples, and computed types.Transform object types by iterating over keys:
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };Model string patterns for event names, CSS values, and API paths:
type EventName = `on${Capitalize<string>}`;
type CSSValue = `${number}${"px" | "rem"}`;`satisfies` for Literal InferenceValidates a value matches a type without widening:
type Config = { url: string; retries: number };
const dev = { url: "http://localhost", retries: 3 } satisfies Config;`extends`: `` — never leave a generic unconstrained.`` .`any``any`. It bypasses the entire type system.`unknown` instead — forces runtime narrowing before use.`never` for exhaustive switch/if-else checking in discriminated unions.`as const` for literal inference on arrays and objects.Error handling pattern with `unknown`:
try {
// ...
} catch (err: unknown) {
if (err instanceof Error) {
console.error(err.message);
}
}`Result` types (discriminated union with `ok` discriminant) for expected failures — avoids try/catch control flow.`throw` for programmer errors (assertions, invariant violations) that should crash.`catch (err: unknown)` and validate shape before use.Never re-export modules through index files. Import directly from source files. This:
`_` prefix or keep them unexported.`asserts x is T`) for runtime validation that narrows types post-return.`// @ts-expect-error` for negative cases.`as T`) — they lie to the compiler. Use type guards instead.| Mistake | Fix |
|---|---|
Using `any` to bypass errors | Use `unknown` + type narrowing |
Overly broad return types (`object`, `Record) | Return specific discriminated unions |
Missing `readonly` on function parameters | `readonly T[]` for array params |
| Optional chaining on non-nullable types | Let the type system guide you — don't over-defend |
| Type assertions instead of type guards | Write a user-defined type guard: `x is T` |