typescript-best-practices ยท diff
git:20260827.f7b391a to git:20260907.b1269d9
8 added, 20 removed. Audit A to A.
---
name: typescript-best-practices
- description: TypeScript best practices. Use when reading or editing any .ts or .tsx file.
+ description: Guide TypeScript type design, boundary validation, and type-safety reviews when implementing or changing TypeScript code.
---
# TypeScript best practices
- Apply the **type-system-discipline** principle skill first; this skill grounds it in TypeScript syntax.
+ Follow the repository's TypeScript conventions and compiler settings. Strengthen types where they prevent a concrete invalid state or unsafe operation; avoid adding type machinery without a caller that needs it.
- | Rule | Summary |
- |------|---------|
- | Discriminated unions | Model variants with a `kind` literal discriminant so impossible states can't be represented. No optional-field bags. |
- | Branded types | Brand primitives with `& { readonly __brand: "X" }` so they can't be mixed up. Validate once at creation. |
- | Constructive modeling | Build the shape so the illegal value can't be constructed. `[T, ...T[]]` for non-empty, `[T, T][]` for even length, `start` plus `duration` for a range. Not a runtime guard, not a wish for refinement types. |
- | Simplest total type | Keep `T[]` while every operation on it stays total. Strengthen to `NonEmpty<T>` only where the loose type forces `!`, a cast, or a "should never happen" throw. |
- | `unknown` over `any` | External data is `unknown`. `any` disables type checking everywhere it touches. |
- | No `as` casts | Every `as` is a runtime crash waiting. Cast only after validation. |
- | Narrowing hierarchy | Discriminant switch > `in` operator > `typeof`/`instanceof` > user-defined type guard > `as`. |
- | Type guards | Must verify the claim. A lying guard is worse than `as` because the bug hides behind a name that says it's safe. Name them `isX` or `hasX`. |
- | Exhaustiveness | Inline `const _exhaustive: never = x;` in default arms so the compiler errors when a new variant is added. |
- | `satisfies` over `as` | Validates the value without widening literal types. |
- | Boundary validation | Parse where data crosses in, into a named domain type. `Record<string, unknown>` (however spelled) stops at that parse. Trust types inside. See the **boundary-discipline** principle skill. |
- | Schema-derived types | Reach for `Pick`/`Omit`/`Parameters`/`ReturnType`/`Awaited`/`typeof` before declaring a new interface. |
- | Object args | Pass objects, not positional, so argument order is self-documenting. Skip on hot paths (per-frame render, tokenizers, parsers). |
- | Real tests | Don't mock what you can run. Prefer the framework's real test primitives with leak/disposable checks, and verify UI in a running build. Mock only what you can't run locally. |
- | Structured telemetry | Prefer structured logger diagnostics with enough context to debug from an id. No `console.log` in shipped code. |
+ - Use discriminated unions for mutually exclusive states and exhaustive handling for closed variants.
+ - Parse external data from `unknown` at trust boundaries. Use established schemas and derive types from their authoritative definitions.
+ - Prefer narrowing or `satisfies` to assertions that hide a mismatch. When an assertion is unavoidable, keep it local to a verified invariant. `as const` is not an unsafe cast.
+ - Use branded primitives when confusing identifiers is a real risk, and non-empty collections only where an operation requires one.
+ - Preserve established function signatures, logging conventions, and test patterns unless the requested change requires otherwise.
- Examples: `references/patterns.md`.
+ Read [patterns](references/patterns.md) when a concrete example is needed. This skill does not require loading a second principle skill or running unrelated runtime tests.