strict: true 是 table stakes。它是 八个 flags,不是整道围栏。arr[i] 仍然是 T。少一个 break 仍然 fall through。enum 仍然 emit 一个 value。
这篇 note 依 Kyle 的 walkthrough。Object model 见 TypeScript Class 与 Runtime Identity。Type-level 那一半见 TypeScript Infer、Extends 与 Ternaries。这篇讲的是 compiler。
1. 八个 flags
strict 打开一个 family。它不会打开那个 family 之后才加进来的全部东西。
strictNullChecks、noImplicitAny、strictFunctionTypes、strictBindCallApplystrictPropertyInitialization、noImplicitThis、useUnknownInCatchVariables、alwaysStrict
那一套是预设前提。新的 TypeScript 把 strict 变成 default。这篇 note 讲的是仍然坐在它 外面 的东西。
这个 repo 的 shared packages(ds、db、auth、intl、content、infra)已经加上 noFallthroughCasesInSwitch、noUncheckedIndexedAccess、noImplicitOverride,以及 verbatimModuleSyntax。apps/web 与 apps/api 大致是 strict 加上 paths。Kyle 没提 verbatimModuleSyntax。这套 stack 已经选了:type-only imports 用 import type。
Failure: 把 strict: true 读成「所有有用的 check」。仍然能 compile 的 crash,通常是 indexed access。
2. Hygiene
这些 flags 抓 typos 与 dead code。它们不改变 arr[i] 的 type。
{
"compilerOptions": {
"paths": { "@/*": ["./src/*"] },
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowUnusedLabels": false,
"noUncheckedSideEffectImports": true,
"noFallthroughCasesInSwitch": true,
"allowUnreachableCode": false
}
}paths是 DX,不是 check。这个 repo 的 web 与 api 已经用@/*。它取代的是相对路径../../../。noUnusedLocals/noUnusedParameters帮从未被读的 binding 画底线。这个 repo 在 packages 里把它们 关掉,apps 也没设。Unused code 在这里是 lint 的工作,不是tsc的工作。allowUnusedLabels: false与allowUnreachableCode: false把预设的 suggestion 升级成 error。Object 外面的name:是 JavaScript label,不是少写的 property。return后面再log是 dead。noUncheckedSideEffectImports在文件叫analytics.ts时,对import "./analytic"报 error。带 typo 的 side-effect import 否则会 silently fail。noFallthroughCasesInSwitch在 packages 里已经开。相邻、共用同一个 body 的caselabels 仍然允许。一个 body 跑进下一个case则不行。
Failure: 在 app code 打开 noUnusedLocals 来「更 strict」。Handler 里没用的 _、没用的 catch binding、stub 里没用的 prop —— lint 加 ignore,比每次 WIP 都吃 compiler error 便宜。
3. Runtime
这三个改变 type 意思什么,或允许怎样 emit。strict 不包含它们。
const numbers = [1, 2, 3]
numbers[10].toString()
// error with noUncheckedIndexedAccess — type is number | undefined
numbers[10]?.toString()
type Settings = {
darkMode: boolean
[key: string]: string | number | boolean
}
const settings: Settings = { darkMode: true, username: "wds" }
settings.darkMode // ok — declared key
settings.username
// error with noPropertyAccessFromIndexSignature
settings["username"] // ok — index signature, on purposenoUncheckedIndexedAccess让arr[i]与record[key]变成T | undefined。TypeScript 不会用 length 对 index。Default 在说谎。这就是strict漏掉的 production crash:Cannot read properties of undefined。这个 repo 的 packages 已经开。Apps 还没有。noPropertyAccessFromIndexSignature切开 syntax。Dot 是 declared key。Brackets 是 index signature。Typosettings.darkMod就 compile 不过。这个 repo 把它 关掉。erasableSyntaxOnly禁止会 emit 一个 value 的 syntax。Constructor parameter properties(constructor(private name: string))会改写 constructor。enum变成 object。Types 必须能干净地 strip —— Bun 与 Node 的 type stripper 不会改写。TypeScript Class 宁用#而不是 TypeScriptprivate,原因相同:#是 JavaScript。
Failure: strict 之后写 users[0].name,而 array 可以是空的。或在 Bun 只做 strip 的文件里用 enum。
4. Preference
有用。不是免费。三个里面两个,这个 repo 的 packages 已经决定。
class Box {
close(): void {}
}
class Modal extends Box {
override close(): void {}
}
type User = { name?: string }
const omitted: User = {}
const explicit: User = { name: undefined }
// error with exactOptionalPropertyTypes — optional means missing, not T | undefined
"name" in omitted // false
"name" in explicit // truenoImplicitOverride要求 child method 取代 parent method 时写override。意外 overwrite 是 class hierarchy 里的 typo。Packages 已经开。Object model 见 TypeScript Class。noErrorTruncation在 error hover 印出完整 type。预设关。为了 debug 一个难搞的 conditional 才打开,然后关掉。Hover 不是 log。exactOptionalPropertyTypes让name?: string表示 key 不存在 或是string。{ name: undefined }不是User。这就是inoperator 的 bug:设成undefined的 key 仍在 object 里。Video 里最 opinionated 的 flag。这个 repo 没设。
Failure: 把 override 当装饰,写在 parent 上根本没有的 method。Flag 检查的是名字仍然对得上。
5. JavaScript
一个还有 .js 文件的 TS repo 需要两个开关。它们不是 TypeScript 的 type safety。
// analytics.js
export const t = 10allowJs让.ts文件 import 那个 module。apps/web与packages/ds已经开。checkJs对每一个.js文件做 type-check。Migration 期间那是洪水。宁可在文件顶一次加一行// @ts-check。当你愿意忽略的.js都没了,再打开checkJs。
Failure: Conversion 第一天就 checkJs: true,然后因为 log 读不下去把 flag 关掉。Per-file comment 才是 migration。
Takeaway
strict 是一个 family。Indexed access、fallthrough、emit,以及 override 坐在它外面。
当问题是该加哪个 flag:
arr[i]有没有在说谎?noUncheckedIndexedAccess。Apps 补上。Packages 已经有。- 这个 syntax 会不会 emit 一个 value?
enum、constructor parameter properties。erasableSyntaxOnly,或者干脆别写。#与 string unions。 - 这是 unused code 还是 typo? Unused 交给 lint。其余用
noFallthroughCasesInSwitch与noUncheckedSideEffectImports。这个 repo 不要把 unused-locals 提升成tsc。