2.2 类型收窄与守卫


2.2 类型收窄与守卫

这节在地图里接在结构化类型之后:兼容性解决"能不能赋",收窄解决"赋进来之后 TS 怎么知道它现在到底是什么"。很多 if 分支里能直接调方法,靠的就是收窄(narrowing)。

为什么需要收窄

unknown 或联合类型进来时,TS 只知道它是"几种可能之一",不能直接当你想要的那一种用。收窄就是提供证据,让 TS 在分支内把类型收紧。

function handle(v: string | number) { // v 此刻是 string | number,下面两个都不能直接调 // v.toUpperCase(); // 报错:number 上没有 if (typeof v === "string") { v.toUpperCase(); // OK:此分支内 v 被收窄为 string } else { v.toFixed(2); // OK:此分支内 v 被收窄为 number } }

输入输出:传入 "hi" 走 string 分支,传入 3 走 number 分支。TS 在编译期就按分支分别认定 v 的类型,这是控制流分析(CFA)的结果。

收窄的几种手段

typeof 管原始类型,instanceof 管类实例,in 管属性存在,=== null 管空值。

// instanceof:区分类 class FileA { read() {} } class FileB { write() {} } function proc(f: FileA | FileB) { if (f instanceof FileA) f.read(); else f.write(); } // in:区分接口形状 interface Bird { fly(): void; } interface Fish { swim(): void; } function move(a: Bird | Fish) { if ("fly" in a) a.fly(); else a.swim(); }

背景:后端常返回"成功对象或错误对象"的联合。操作:用 intag 字段区分。结果:分支内获得精确类型。解读:这是可辨识联合的雏形(2.3 展开)。变式:数组 filter 配合类型谓词可同时收窄。

自定义类型守卫:用户态的收窄证明

当内置手段不够,用类型谓词 arg is T 自己声明"这个函数返回 true 时就收窄成 T"。

interface User { kind: "user"; id: number; } interface Admin { kind: "admin"; id: number; level: number; } type Account = User | Admin; function isAdmin(a: Account): a is Admin { return a.kind === "admin"; } function describe(a: Account) { if (isAdmin(a)) { a.level; // OK:收窄为 Admin } else { a.id; // OK:收窄为 User } }

a is Admin 是给编译器的承诺:函数返回 true 时,参数 a 就是 Admin。这个承诺 TS 不会验证对错,写错了会误导类型——所以谓词函数内部判断要准。

可辨识联合的收窄(重点)

给每个成员加一个字面量 tag 字段,TS 就能在 switch 里精确收窄,这是建模状态的利器。

type Result = | { status: "ok"; data: string } | { status: "error"; message: string } | { status: "loading" }; function render(r: Result) { switch (r.status) { case "ok": r.data; break; // 收窄为 ok 分支 case "error": r.message; break; // 收窄为 error 分支 case "loading": break; // 收窄为 loading } }

这张图把"收窄如何在分支里分裂类型"画出来:

02-02-fig01-2

断言式收窄:as 与非空断言

走投无路时用 as 强制告诉 TS 类型,但放弃了它的检查。

const el = document.getElementById("app") as HTMLElement; // 假设它一定存在 const v = maybeNull!; // 非空断言,告诉 TS 这里不为 null

输入输出:这两段编译都过。运行时若 getElementById 返回 null,第一句后面 . 操作就炸。as! 是把契约责任从编译器转到你自己肩上,能不用就不用。

等式收窄的隐含前提

=== 也能收窄,但前提是 TS 知道被比较的常量来自哪个字面量集合。常量提升(const)与字面量类型是这里的关键。

declare const mode: "dev" | "prod"; function run(m: typeof mode) { if (m === "dev") { // 此分支 m 收窄为字面量 "dev" console.log("开发态"); } } // 若 mode 被声明为普通 string,m === "dev" 无法收窄成字面量 declare const loose: string; function run2(m: string) { if (m === "dev") { // m 仍是 string,没有更窄 } }

背景:配置开关、环境判断常这么写。操作:把模式变量标成字面量联合而非 string。结果:分支内拿到精确字面量,可进一步喂给可辨识联合。解读:收窄的强度取决于"比较两侧类型信息的精度",源头标注含糊,下游收窄就弱。

赋值收窄与 const 的精读

letconst 在收窄语义上不同:const 的引用不可改,所以 TS 能把它锁定成精确字面量;let 后续可能重赋,字面量会放宽成基类型。

const tag = "ready"; // 类型 "ready"(字面量) let flag = "ready"; // 类型 string(可重赋,放宽) type State = "ready" | "pending"; function set(s: State) { // flag = s; // OK:flag 是 string,能接 State // tag = s; // 报错:tag 是字面量 "ready",不可改也不可接其它值 }

这张差异在"配置常量为字面量联合"时尤其有用:用 const 声明枚举式常量,能避免意外被改成非法值,也让 switch 收窄更干净。

工程取舍:守卫函数 vs 直接 as

同一个"判断是不是 Admin",写 isAdmin 守卫能让后续每个调用点都自动收窄,写一次受益多处;直接用 as Admin 则每处都要写、且每次都放弃检查。中型以上项目,守卫函数的长期维护成本明显更低。

本节要点回顾

  • 收窄靠控制流分析,在分支内自动收紧类型
  • 手段:typeof/instanceof/in/相等判断/自定义守卫
  • 可辨识联合(字面量 tag)是状态建模首选
  • as! 放弃检查,慎用作兜底

⚠️ 类型谓词 a is T 是 TS 信任你的"口头承诺",它不验证函数体是否真能推出 T。写错会让类型比 any 还危险,因为你看不出破绽。

💡 处理"成功/错误"这类返回时,优先用可辨识联合而非抛异常,调用方在编译期就被迫处理错误分支,比 try/catch 漏写更不容易出事。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U