TypeScript Guardrails
These are hard rules. Check every piece of TypeScript you write, review, or modify against this list.
Types
DO NOT use
enum. Useas constobjects + inferred type instead.// wrong enum Status { Active, Inactive } // correct const STATUS = { Active: 'active', Inactive: 'inactive' } as const; type Status = typeof STATUS[keyof typeof STATUS];DO NOT use
any. Useunknownwith type guards to narrow.Prefer
typeoverinterfaceunless you need declaration merging,extends, orimplements.DO NOT use type assertions (
as X) unless no alternative exists. Narrow with control flow instead. If unavoidable, add a comment explaining why.- Only exception:
as Record<string, unknown>inside type guards, to access properties on a narrowed object. No otherascasts — validate withtypeof,in, or equality checks. When checking membership in a const object, widen the array type toReadonlyArray<string>instead of casting the value:function isUser(value: unknown): value is User { if (typeof value !== 'object' || value === null) return false; const obj = value as Record<string, unknown>; // only acceptable assertion const validRoles: ReadonlyArray<string> = Object.values(USER_ROLE); return typeof obj.name === 'string' && typeof obj.role === 'string' && validRoles.includes(obj.role); // ↑ widen to ReadonlyArray<string> — do NOT cast obj.role as SomeType }
- Only exception:
Use
satisfiesfor type-safe object literals where you want inference + validation.const config = { port: 3000, host: 'localhost' } satisfies ServerConfig;Explicit return types on all exported functions.
Imports / Exports
No barrel files (
index.tsre-exports) except designated package entry points.Explicit import paths —
import { bar } from './utils/bar', notfrom './utils'.No default exports except config files (
vite.config.ts,eslint.config.ts, etc.). Use named exports everywhere else.ESM only —
import/export. Neverrequire()ormodule.exports.
Strict Mode
"strict": truein tsconfig — always. Do not loosen individual strict flags."noUncheckedIndexedAccess": truewhen the project supports it.DO NOT use
@ts-ignore. Use@ts-expect-errorwith an explanation comment if suppression is truly needed.DO NOT use non-null assertions (
!) unless provably safe. If used, add a comment explaining the proof.
Patterns
constby default.letonly when reassignment is needed. Nevervar.Exhaustive switches — always include a
defaultcase with anevercheck:default: { const _exhaustive: never = value; throw new Error(`Unhandled case: ${_exhaustive}`); }Discriminated unions over type predicates when modeling variants.
Readonly by default — use
readonlyproperties,ReadonlyArray<T>,Readonly<T>.No classes unless required by a framework (e.g., Angular, NestJS). Prefer functions + closures.
Naming
| Thing | Convention | Example |
|---|---|---|
| Files | kebab-case | user-service.ts |
| Types / Interfaces | PascalCase | UserProfile |
| Functions / variables | camelCase | getUserById |
| Constants | UPPER_SNAKE_CASE | MAX_RETRY_COUNT |
| Schema interfaces | <Name>Schema |
UserSchema |
Error Handling
Never swallow errors — no empty
catchblocks. Log, rethrow, or handle explicitly.Typed errors — use custom error classes or discriminated union result types.
Avoid
throwin library code when possible. Prefer a Result pattern or explicit error returns:type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };