TS 沿着你的 if/switch 逐块缩小类型: 判别字段一动, 联合只剩一个 — 漏分支交给 never 举报
收窄就是 TS 的读心术: 你写 if (typeof x === 'string'), 它就在这个分支里把 x 当 string 用, 在 else 里把你排除掉的类型踢出去。不需要 as、不需要注释 — 因为你的判断本身就是证据, TS 只是沿控制流忠实地推理。判别联合则把这件事制度化: 所有变体共享一个判别字段(kind/type/status), switch 它一下, 每个分支的形状自动就位, 多余字段、缺失分支全部现形。
读心术有盲区: 延迟执行的代码(回调、await 之后)里, TS 不敢保证"那时候"变量还是收窄后的样子 — 存个 const 快照即可。而 unknown 是这套体系的安全入口: 什么都进得来, 但用之前必须给出证据(收窄), 这正是外部数据的正确通道。
let id: string | number = 1; id.toUpperCase(); // ✗ number 没有 toUpperCase if (typeof id === 'string') id.toUpperCase(); // ✓
typeof null === 'object' 的坑, 先排除 null。 function f(x: string | number) { if (typeof x === 'number') x.toFixed(); else x.toUpperCase(); // else 分支剩 string }
class Err4xx extends Error { code = 400 } if (e instanceof Err4xx) e.code; // ✓ 收窄到子类
type A = { url: string }; type B = { text: string }; if ('url' in msg) msg.url; // ✓ 分支内是 A
type Res = | { status: 'ok'; data: unknown } | { status: 'err'; msg: string };
switch (s.kind) { case 'a': return; case 'b': return; default: const _x: never = s; // 漏 'c' 就编译错 }
x is T 让自定义布尔函数携带收窄能力; 函数体内返回值必须与判断一致。 const isStr = (x: unknown): x is string => typeof x === 'string'; if (isStr(v)) v.toUpperCase(); // ✓ 收窄生效
asserts x is T: 不满足直接 throw, 调用点之后视为已收窄 — 适合守卫式封装。 function assertUser(x: unknown): asserts x is User { if (!isUser(x)) throw new Error('not user'); } assertUser(data); data.id; // 之后全是 User
?. 是两个方向。 user!.name; // user 为 null 时运行时炸 user?.name ?? ''; // 安全读取+默认值, 优先这个
if (user) { await save(); user.name; // ✗ 严格下收回宽类型 // 对: const u = user 先存, 后续用 u }
const body: unknown = await req.json(); // any 版: body.x 直接用, 炸在运行时 // unknown 版: 必须先 isUser(body) 才许摸属性
const routes = ['home', 'about'] as const; type Route = typeof routes[number]; // 'home' | 'about'
接口层返回 {code,data} 全靠口头约定, 忘判 code 直接读 data 就是 undefined。判别联合把"必须判断"写进类型:
type ApiRes<T> = | { ok: true; data: T } | { ok: false; status: number; msg: string }; async function get<T>(url: string): Promise<ApiRes<T>> { /* ... */ } const r = await get<User>('/me'); if (!r.ok) return redirect(r.status); // 不判这个, 下一行编译不过 r.data.name; // 这里 TS 保证 r.ok 为 true
新增"已归档"状态后, 老的 if-else 链静默漏算。判别联合 + never 让漏分支编译期报警:
type Status = | 'created' | 'paid' | 'shipped' | 'archived'; function canCancel(s: Status) { switch (s) { case 'created': case 'paid': return true; case 'shipped': case 'archived': return false; default: { const _x: never = s; return false; } } } // 以后加状态漏写 case → 编译失败, 不是线上事故
ws 推送 10 种消息, onmessage 里一个 any 闭眼转发。判别联合分发到各自 handler:
type Msg = | { type: 'tick'; price: number } | { type: 'fill'; orderId: string; qty: number }; ws.onmessage = e => { const m = JSON.parse(e.data) as Msg; // 边界 as (配 zod 更稳) switch (m.type) { case 'tick': updatePrice(m.price); break; case 'fill': settle(m.orderId, m.qty); break; } };
动态表单的字段定义是联合, 判别字段 variant 决定渲染哪个控件, 控件 props 精确匹配:
type Field = | { variant: 'text'; maxLength: number } | { variant: 'select'; options: string[] }; function renderField(f: Field) { switch (f.variant) { case 'text': return <Input maxLength={f.maxLength} />; case 'select': return <Select options={f.options} />; } } // text 分支拿不到 options, select 分支拿不到 maxLength
as User 是自欺欺人。zod 在运行时验证, safeParse 结果天然是判别联合:
const UserSchema = z.object({ id: z.number(), name: z.string() }); const r = UserSchema.safeParse(json); if (!r.success) { return badRequest(r.error.issues[0].message); // err 分支 } r.data; // ✓ 成功分支, 类型是 { id: number; name: string } type User = z.infer<typeof UserSchema>; // schema 单源出类型
20 处"if (!user) throw"散落各处。asserts 谓词收口一处, 调用后自动收窄:
function assertDefined<T>( v: T | undefined | null, msg: string ): asserts v is T { if (v == null) throw new Error(msg); } assertDefined(ctx.user, '需要登录'); ctx.user.id; // ✓ 已收窄, 无需 if
[1, undefined, 2].filter(Boolean) 的返回仍是 (number|undefined)[]。谓词版本真正收窄:
const xs: (number | undefined)[] = [1, undefined, 2]; const bad = xs.filter(Boolean); // 仍是 (number | undefined)[] const ok = xs.filter((x): x is number => x != null); ok; // → number[] ✓
缓存值类型跟键走, 判别联合 + 映射让 get 返回值精确:
type CacheMap = { user: User; order: Order }; type CacheKey = keyof CacheMap; function get<K extends CacheKey>(k: K): CacheMap[K] | undefined { return store.get(k) as CacheMap[K] | undefined; } const u = get('user'); // → User | undefined 精确
老接口函数全 any, 一次改不动。先把 any 换成 unknown(更诚实), 再逐调用点收窄:
// 第 1 步: (o: any) => (o: unknown) — 调用方开始报错, 列出所有触点 // 第 2 步: 每个触点补收窄 function total(o: unknown) { if (isOrder(o)) return o.items.reduce((s, i) => s + i.price, 0); throw new TypeError('not an order'); } // any 是"关灯", unknown 是"装了门禁" — 迁移方向永远是后者
事件回调里读了被收窄的可变量, 严格模式报错。进回调前先定格:
function onChange(s: State) { if (s.stage !== 'editing') return; const draft = s; // 快照: editing 态的引用 return () => commit(draft.draftDoc); // 回调里用快照 }
const 快照。 // 错: if (x !== 'a') return; cb.push(() => f(x)); // x 回宽 // 对: const v = x; cb.push(() => f(v));
// 错: if (typeof v === 'object') v.x; // null 混进来了 // 对: if (v !== null && typeof v === 'object') v.x;
// 错: e instanceof UserIface // 编译过但恒 false // 对: 'userId' in e 或 isUser(e)
// 错: { kind?: 'a' } | { kind?: 'b' } // 对: { kind: 'a' } | { kind: 'b' }
(x): x is T =>。 xs.filter(Boolean); // 宽类型 xs.filter((x): x is T => x != null); // ✓ 窄
// 错: const u = json as User; // json 可能是任何东西 // 对: const u = UserSchema.parse(json);
x as unknown as Payment; // 检查全灭, 与 any 无异
?. + ?? 或先判空。 config!.db!.host!; // 三连 ! = 三颗雷 config?.db?.host ?? '';
const ks = Object.keys(o) as (keyof typeof o)[]; // 常用模式
if (u) { await x(); u.id; } // 严格下报错 const uu = u; if (uu) { await x(); uu.id; } // ✓
// 对: default: { const _x: never = s; break; }
// 错: const isU = (x): x is User => !!x; // 太宽 // 对: 校验关键结构后再返回 true
// { url?: string } | { url?: number } — 'url' in 两边都过 // 对: 换 kind: 'img' | 'text' 判别
list.map(m => { if (m.kind === 'a') use(m); }); // 分支内收窄default: { const _x: never = s; throw new Error('unreachable'); }
const Kind = { a: 'a', b: 'b' } as const; type Kind = typeof Kind[keyof typeof Kind]; // 'a' | 'b'
const r = res satisfies ApiRes; if (r.ok) use(r.data); // 收窄逻辑不能省
let v: A | any = x; // v 就是 any, 全白写
// 每个 switch over 联合 都该有 never 兜底// 错: x is User 只查 'id' in x // 对: id/name/role 全查, 不确定的走 schema.parse