起因
老项目 JS(或松散 TS)想拧紧 strict mode:
noImplicitAnystrictNullChecksstrictFunctionTypesnoImplicitThisstrictPropertyInitializationalwaysStrict
一次性全开 → 几千 type error → 团队崩。
渐进 rollout 是正解。
strict 全开
{
"compilerOptions": {
"strict": true,
// 等价于全开上面 6 个
}
}
新项目直接 strict。
老项目分步骤
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true,
"strictNullChecks": false,
...
}
}
一个一个加,每个加完跑全 ts check + 修。
推荐顺序:
noImplicitAny(多数 type 自动推或 explicit)strictNullChecks(最大改动,但最值)strictFunctionTypes- 其它
strictNullChecks 影响
// 关 strictNullChecks
function getName(user) { // user: any
return user.name; // 没事
}
getName(null); // 没报,但运行时崩
// 开
function getName(user: User | null) {
return user.name; // ❌ Object is possibly 'null'
}
function getNameSafe(user: User | null) {
return user?.name; // ✅
}
每函数 explicit handle null。
最大价值:编译期 catch null pointer 问题。
渐进:per-file strict
@ts-strict 风格 comment / 工具:
// @ts-check
// @ts-strict
或者用 ts-strict-plugin、单 file // @ts-expect-error 标技术债。
TypeScript per-file
更激进:拆 tsconfig 多个 project:
// tsconfig.strict.json
{
"extends": "./tsconfig.json",
"compilerOptions": { "strict": true },
"include": ["src/strict/**"]
}
src/strict/ 下严格,其余宽。
新代码进 strict,老代码慢慢迁。
工具帮迁移
ts-migrate(Airbnb 出):批量 JS → TS + 加// @ts-expect-errortypescript-strict-plugin:渐进 strict per-file
渐进示例
Week 1: 装 TS, allowJs: true, 跑 baseline
Week 2: noImplicitAny on, 修 (100 error)
Week 3: 把 critical paths 改 strict (per-file)
Week 4: strictNullChecks on, 修 (500 error,多数小)
Month 2: 全 strict
常见迁移 pattern
// 之前
function process(data) {
if (data.user.address.city) { ... }
}
// 改 1: 类型
interface User { address?: Address }
interface Address { city?: string }
function process(data: { user: User }) {
if (data.user.address?.city) { ... } // optional chaining
}
// 改 2: 早返
function process(data: { user: User }) {
const city = data.user.address?.city;
if (!city) return;
// city 类型 narrowed string
}
optional chaining + early return 是最常用 strict-friendly pattern。
第三方 lib 没 type
// 装 @types
npm install -D @types/lodash
// 没 @types 的
declare module 'weird-lib' {
export function doStuff(x: any): any;
}
最差情况 cast:
const lib = require('weird-lib') as any;
但 as any 会传染 → 限定到边界。
any vs unknown
function parse(input: any) { // 危险
return input.value; // 不报,但运行时崩
}
function parse(input: unknown) { // 强制处理
if (typeof input === 'object' && input && 'value' in input) {
return input.value; // narrowed
}
}
unknown 是 type-safe 版 any。强制 narrow 后用。
新代码用 unknown 替代 any。
strictNullChecks 后的常见错
// 1. array access
const arr: string[] = [];
const first: string = arr[0]; // ❌ undefined if empty
// noUncheckedIndexedAccess: true
const first: string | undefined = arr[0];
// 2. delete
const obj: { x?: number } = { x: 1 };
delete obj.x; // 必须先 optional
// 3. JSON.parse
const data: unknown = JSON.parse(s);
// 不能直接 data.x → Zod / type guard
CI gate
// package.json
"scripts": {
"typecheck": "tsc --noEmit",
"typecheck:strict": "tsc --project tsconfig.strict.json --noEmit"
}
# CI
- run: pnpm typecheck
- run: pnpm typecheck:strict # 只检查 strict files
PR 改动 strict file → 必须过 strict check。
慢慢"扩 strict 边界"。
真实迁移
某老项目 (50k LOC JS):
- 全开 strict 一次:3000 error,回归测试一周
- 改渐进:4 个月
- 月 1:装 TS + allowJs,0 改 → 跑通
- 月 2:noImplicitAny + 主要路径手加 type → 50% 文件 typed
- 月 3:strictNullChecks,整修 → 1500 changes
- 月 4:全 strict + tslint → strict
- 期间也持续开发新 feature
迁完收益:
- 生产 NullPointer error 降 80%
- IDE auto-complete 准
- 重构信心大增
- 新人 onboarding 快(看类型就懂 API)
skipLibCheck
{
"skipLibCheck": true // 第三方 .d.ts 不 check
}
大型项目几乎必开。第三方 type 偶有小问题不卡你。
不要追求 100%
any 不是恶魔。
边界(unknown API / migration code / quick prototype)用 any 标
// FIXME: type later。
内部核心 type-tight 90% 已经收益大头。
与 jsdoc TS 对比
// JS file + JSDoc
/**
* @param {string} name
* @returns {string}
*/
function greet(name) {
return `Hi ${name}`;
}
// @ts-check 让 TS check JSDoc 类型。
适合:不想转 .ts 但想要 type-check(如 lib / 小项目)。
中大型 → 直接 .ts。
踩过的坑
-
Object.keys返回 string[]:Object.keys({a:1, b:2}).forEach((k) => obj[k])报 string can't index Foo。(Object.keys(obj) as (keyof Foo)[])
cast。 -
DOM null:
document.querySelector('.x')返 Element | null。
!non-null assert 慎用。 -
callback this:strict 模式下 method as callback 失
this。
bind / arrow。 -
enum 类型:
enum X { A, B }编译有运行时 object。
const X = { A: 'a', B: 'b' } as const; type X = (typeof X)[keyof typeof X]
更轻。 -
复杂 generic:递归 / mapped type 编译慢。简化或拆。
登录后参与评论。