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。