typescript-best-practices
TypeScript 最佳实践
Apply the type-system-discipline principle skill first.
先应用 type-system-discipline principle skill。
| 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 the boundary. |
| 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. |
| Schemas before guards | Before hand-writing a property-by-property type guard, use the repository’s runtime schema library and infer the type from the schema, such as z.infer. |
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. |
| 规则 | 摘要 |
|---|---|
| Discriminated unions | 用 kind 字面判别式建模变体,让不可能状态不可表示。不要 optional 字段袋。 |
| Branded types | 用 & { readonly __brand: "X" } 给原语 branding,避免混用。在边界校验一次。 |
| Constructive modeling | 建成无法构造非法值的形状。非空用 [T, ...T[]],偶长用 [T, T][],范围用 start + duration。不是运行时守卫,也不是对 refinement type 的愿望。 |
| Simplest total type | 操作保持全时继续用 T[]。只在松类型逼出 !、cast 或「绝不该发生」throw 的地方加强到 NonEmpty<T>。 |
unknown over any | 外部数据是 unknown。 |
| Schemas before guards | 手写逐属性 type guard 前,先用仓库的运行时 schema 库并从 schema 推断类型,如 z.infer。 |
No as casts | 每个 as 都是等着的运行时崩溃。只在校验后再 cast。 |
| Narrowing hierarchy | 判别式 switch > in > typeof/instanceof > 用户 type guard > as。 |
| Type guards | 必须验证主张。撒谎的 guard 比 as 更糟,因为 bug 藏在「安全」的名字后面。命名 isX 或 hasX。 |
| Exhaustiveness | 在 default 臂内联 const _exhaustive: never = x;,新变体加入时编译器报错。 |
satisfies over as | 校验值且不拓宽字面量类型。 |
| Boundary validation | 数据跨入处解析成命名领域类型。Record<string, unknown>(无论怎么写)停在那次解析。内部信任类型。见 boundary-discipline。 |
| Schema-derived types | 声明新 interface 前先找 Pick/Omit/Parameters/ReturnType/Awaited/typeof。 |
| Object args | 传对象,别传位置参数,让参数顺序自说明。热路径跳过(逐帧渲染、tokenizer、parser)。 |
| Real tests | 能跑的别 mock。优先框架真实测试原语加泄漏/可释放检查,在正在跑的构建里验证 UI。只 mock 本地跑不了的。 |
| Structured telemetry | 优先结构化 logger 诊断,带够从 id 调试的上下文。交付代码里禁止 console.log。 |
Examples: references/patterns.md.
示例见:references/patterns.md。