typescript-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TypeScript best practices

TypeScript 最佳实践

Apply the type-system-discipline principle skill first.
RuleSummary
Discriminated unionsModel variants with a
kind
literal discriminant so impossible states can't be represented. No optional-field bags.
Branded typesBrand primitives with
& { readonly __brand: "X" }
so they can't be mixed up. Validate once at the boundary.
Constructive modelingBuild 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 typeKeep
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 guardsBefore 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 hierarchyDiscriminant switch >
in
operator >
typeof
/
instanceof
> user-defined type guard >
as
.
Type guardsMust 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
.
ExhaustivenessInline
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 validationParse 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 typesReach for
Pick
/
Omit
/
Parameters
/
ReturnType
/
Awaited
/
typeof
before declaring a new interface.
Object argsPass objects, not positional, so argument order is self-documenting. Skip on hot paths (per-frame render, tokenizers, parsers).
Real testsDon'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 telemetryPrefer structured logger diagnostics with enough context to debug from an id. No
console.log
in shipped code.
Examples:
references/patterns.md
.
首先应用 type-system-discipline 原则 skill。
规则摘要
判别联合(Discriminated unions)使用
kind
字面量判别符对变体建模,使不可能的状态无法被表示。不要使用仅包含可选字段的数据结构。
品牌类型(Branded types)使用
& { readonly __brand: "X" }
为原始值添加品牌,防止它们被混用。在边界处验证一次。
构造式建模(Constructive modeling)通过类型结构让非法值无法被构造。非空使用
[T, ...T[]]
,偶数长度使用
[T, T][]
,范围使用
start
加
duration
。它不是运行时 guard,也不是对 refinement types 的奢望。
最简单的全函数类型(Simplest total type)只要其上的每个操作都是 total,就保留
T[]
。仅当宽松类型迫使你使用
!
、cast 或抛出“绝不应发生”的异常时,才收紧为
NonEmpty<T>
。
unknown
优于
any
外部数据应视为
unknown
。
Schema 优先于类型守卫在手写逐属性类型守卫之前,使用仓库的运行时 schema 库,并从 schema 推断类型,例如
z.infer
。
禁止
as
cast
每个
as
都是在给运行时崩溃埋下伏笔。只能在验证之后 cast。
收窄层级(Narrowing hierarchy)判别符 switch >
in
运算符 >
typeof
/
instanceof
> 用户自定义类型守卫 >
as
。
类型守卫(Type guards)必须验证所作的断言。会说谎的类型守卫比
as
更糟糕,因为 bug 会藏在一个声称安全的名称背后。将它们命名为
isX
或
hasX
。
穷尽性(Exhaustiveness)在 default 分支中内联
const _exhaustive: never = x;
,这样添加新变体时编译器就会报错。
satisfies
优于
as
在不拓宽字面量类型的情况下验证值。
边界验证(Boundary validation)在数据进入系统时进行解析,并转换为命名的领域类型。
Record<string, unknown>
(无论采用何种写法)止步于这次解析。进入内部后信任这些类型。参见 boundary-discipline 原则 skill。
Schema 派生类型(Schema-derived types)声明新的 interface 之前,先考虑
Pick
/
Omit
/
Parameters
/
ReturnType
/
Awaited
/
typeof
。
对象参数(Object args)传递对象而不是位置参数,使参数顺序一目了然。在 hot path 中不要这样做(逐帧渲染、tokenizer、parser)。
真实测试(Real tests)凡是能够实际运行的部分都不要 mock。优先使用框架真实的测试原语,并进行 leak/disposable 检查;在运行中的构建中验证 UI。只 mock 无法在本地运行的部分。
结构化遥测(Structured telemetry)优先使用包含足够上下文的结构化 logger 诊断信息,以便仅凭 id 就能进行调试。正式发布的代码中不要使用
console.log
。
示例:
references/patterns.md
。