ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TypeScript 类型缩小(Type Narrowing)完全指南:类型守卫、控制流分析与可辨识联合

TypeScript 类型缩小(Type Narrowing)完全指南:类型守卫、控制流分析与可辨识联合 文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载导读类型缩小是 TypeScript 类型系统的核心机制之一它让编译器在if/else、switch、三元表达式、循环与真值检查等 JavaScript 运行时控制流构造之上叠加静态类型分析在程序的不同路径上把联合类型如number | string细化到比声明更具体、更精确的类型。本文以本仓库的中文手册 narrowing.md 为主体脉络系统讲解typeof、真值、等式、in、instanceof五大内置类型守卫赋值语句与控制流分析、用户自定义类型断言、断言函数、可辨识联合以及基于never的完备性检查并结合仓库内 compiler-options.md、classes.md、typescript-3.7.md 等文档给出源码级佐证。读完本文你将能写出“看起来就是普通 JavaScript、但类型安全滴水不漏”的 TypeScript 代码并掌握用可辨识联合 never做穷尽性exhaustive检查的工程实践。一、为什么需要类型缩小从一个padLeft开始假设我们要写一个函数padLeft当padding是number时把它当作在input前面填充的空格数当padding是string时直接把它拼到input前面。function padLeft(padding: number | string, input: string): string { throw new Error(尚未实现); }如果只写出“对 number 分支”的逻辑// errors: 2345 function padLeft(padding: number | string, input: string) { return .repeat(padding) input; }TypeScript 会立刻报错错误码 2345padding的类型是number | string而String.prototype.repeat只接受number。这正是编译器在要求我们显式区分两种情况。加上类型守卫后问题消失function padLeft(padding: number | string, input: string) { if (typeof padding number) { return .repeat(padding) input; } return padding input; }这段代码看起来和普通 JavaScript 毫无区别——这正是 TypeScript 类型系统的设计目标让你写典型 JavaScript 时不必费力换取类型安全。手册原文强调TypeScript 不仅做静态类型分析还会把类型分析叠加在if/else、条件三元运算符、循环、真值检查等运行时控制流构造上。其中typeof padding number这类特殊检查被称为类型守卫type guardTypeScript 沿着程序可能执行的路径把某个位置的值分析为“在给定位置最具体的可能类型”的过程就叫类型缩小narrowing。在编辑器里把鼠标悬停在padding上原文中的^?标记就能观察到类型从number | string缩小为numberif 分支内和stringif 之后。术语澄清联合类型number | string的基础语法可参考手册 everyday-types.mdstrictNullChecks等与本主题强相关的编译选项详见 compiler-options.md。二、typeof类型守卫JavaScript 的typeof运算符能在运行时给出值的基本类型信息。TypeScript 期望它返回以下八种字符串之一stringnumberbigintbooleansymbolundefinedobjectfunctiontypeof在众多 JavaScript 库中反复出现TypeScript 能理解它并据此在不同分支中缩小类型。但 TypeScript 也对 JavaScript 的“怪异行为”做了编码——typeof null返回的是object列表里根本没有null这个字符串。手册用printAll演示了这个陷阱// errors: 2531 18047 function printAll(strs: string | string[] | null) { if (typeof strs object) { for (const s of strs) { console.log(s); } } else if (typeof strs string) { console.log(strs); } else { // 什么都不做 } }这里我们本想用typeof strs object判断strs是不是数组数组在 JavaScript 中属于对象类型但null也会落入object分支。幸运的是 TypeScript 把strs缩小为string[] | null而不是string[]于是在for...of处报出2531对象可能为 null和18047对象不可迭代错误提醒我们null未被排除。三、真值缩小Truthiness Narrowing在 JavaScript 中条件语句、、||、if、布尔否定!都接受任意表达式if并不要求条件一定是boolean类型。像0、NaN、空字符串、0nbigint 的零、null、undefined都会被强制转换为false其余值转为true。例如function getUsersOnlineMessage(numUsersOnline: number) { if (numUsersOnline) { return 现在有 ${numUsersOnline} 人在线; } return 这里没有人。 :(; }把值显式转换为布尔有两个常用写法Boolean(value)推断出的类型是宽泛的boolean和双重否定!!valueTypeScript 能推断出更窄的字面量布尔类型true// 这两个都会得到 ‘true’ Boolean(hello); // 类型: boolean, 值: true !!world; // 类型: true, 值: true真值检查最常见的用途是防范null/undefined。给printAll加上真值检查即可消除上面的错误function printAll(strs: string | string[] | null) { if (strs typeof strs object) { for (const s of strs) { console.log(s); } } else if (typeof strs string) { console.log(strs); } }这至少避免了运行时出现TypeError: null is not iterable这类可怕错误。但手册同时强调对基本类型做真值检查往往容易出错。比如把整个函数体都包进if (strs)// 不要这样做 function printAll(strs: string | string[] | null) { if (strs) { if (typeof strs object) { for (const s of strs) { console.log(s); } } else if (typeof strs string) { console.log(strs); } } }这样做的微妙缺点是空字符串是假值会被外层if (strs)过滤掉导致printAll()什么也不打印。TypeScript 对此无能为力——它不会替我们规定“对某个值应该做什么”此时可以借助代码检查工具linter来确保覆盖这类情况。最后布尔否定!会把被否定的值过滤到否定分支。看multiplyAll在if (!values)分支里values被缩小为undefined因此可以直接return values而在else分支里它是干净的number[]可以安全调用.mapfunction multiplyAll( values: number[] | undefined, factor: number ): number[] | undefined { if (!values) { return values; } else { return values.map(x x * factor); } }四、等式缩小Equality NarrowingTypeScript 同样利用switch语句和等式检查、!、、!来缩小类型。关键洞察是如果两个变量的类型相等那么它们的公共类型就是各自类型的交集function example(x: string | number, y: string | boolean) { if (x y) { // 现在我们可以在 x 或 y 上调用任何 string 方法。 x.toUpperCase(); y.toLowerCase(); } else { console.log(x); console.log(y); } }x与y唯一的公共类型是string所以在x y为真的分支里两者都被缩小为string。检查特定的字面值同样有效。前面printAll因空字符串问题容易出错改用显式的strs ! null检查就能精确排除nullfunction printAll(strs: string | string[] | null) { if (strs ! null) { if (typeof strs object) { for (const s of strs) { console.log(s); } } else if (typeof strs string) { console.log(strs); } } }JavaScript 的宽松等式/!也能正确缩小类型而且有个很有用的语义x null不仅匹配字面量null还匹配undefined反之亦然x undefined同样匹配null或undefined。这一特性常被用来一次性剔除可空值interface Container { value: number | null | undefined; } function multiplyValue(container: Container, factor: number) { // 从类型中移除 null 和 undefined。 if (container.value ! null) { console.log(container.value); // 现在我们可以安全地将 container.value 乘以 factor。 container.value * factor; } }五、in运算符缩小JavaScript 的in运算符用于判断对象自身或其原型链上是否存在指定名称的属性。TypeScript 把它当作缩小类型的手段对value in xvalue是字符串字面量x是联合类型“true”分支会把x缩小为具有必需或可选value属性的类型“false”分支则缩小为value属性可选或缺失的类型type Fish { swim: () void }; type Bird { fly: () void }; function move(animal: Fish | Bird) { if (swim in animal) { return animal.swim(); } return animal.fly(); }注意可选属性在in检查的两个分支中都会出现。人可以游泳可选也会飞可选因此在两个分支里都应存在type Fish { swim: () void }; type Bird { fly: () void }; type Human { swim?: () void; fly?: () void }; function move(animal: Fish | Bird | Human) { if (swim in animal) { animal; // 类型为 Fish | Human } else { animal; // 类型为 Bird | Human } }六、instanceof缩小x instanceof Foo检查x的原型链是否包含Foo.prototype。它同样是一种类型守卫在被instanceof保护的分支里TypeScript 会缩小类型范围。instanceof对一切可以用new构造的值都很有用类的详细内容见 classes.mdfunction logValue(x: Date | string) { if (x instanceof Date) { console.log(x.toUTCString()); // x: Date } else { console.log(x.toUpperCase()); // x: string } }七、赋值语句与缩小对任何变量赋值时TypeScript 都会查看赋值语句的右侧并据此缩小左侧的类型——但这只是“观察到的当前类型”let x Math.random() 0.5 ? 10 : hello world!; // x: number | string x 1; console.log(x); // x: number x goodbye!; console.log(x); // x: string每次赋值都有效尽管第一次赋值后x的观察类型是number我们依然可以给它赋string。原因在于可赋值性始终基于x的声明类型string | number检查。反过来给x赋boolean就会报错错误码 2322因为boolean不在声明类型之中// errors: 2322 let x Math.random() 0.5 ? 10 : hello world!; x 1; console.log(x); x true; // ❌ 类型 boolean 不能赋值给类型 string | number八、控制流分析可达性与路径分裂前面的例子暗示了TypeScript 并不只是“从每个变量开始向上查找类型守卫所在的if/while/条件语句”而是真正做基于可达性的流分析。再看padLeftfunction padLeft(padding: number | string, input: string) { if (typeof padding number) { return .repeat(padding) input; } return padding input; }函数在第一个if块内returnTypeScript 分析出在padding是number的情况下函数体剩余部分return padding input;不可达于是它能把number从剩余代码中padding的类型里移除将string | number缩小为string。这种基于可达性的分析就是控制流分析control flow analysis。分析一个变量时控制流可以一次又一次地分裂分支与重新合并汇合变量在每个程序点上都可能拥有不同的类型function example() { let x: string | number | boolean; x Math.random() 0.5; console.log(x); // x: boolean if (Math.random() 0.5) { x hello; console.log(x); // x: string } else { x 100; console.log(x); // x: number } return x; // x: string | number }example的返回类型正是控制流分析精度的直接体现两个分支分别把x赋成string与number汇合后boolean已被排除。九、用户自定义类型守卫类型断言parameterName is Type内置的六类守卫typeof、真值、等式、in、instanceof、赋值之外有时我们需要更直接地控制类型变化。此时可以定义用户自定义类型守卫——一个返回类型为类型断言的函数type Fish { swim: () void }; type Bird { fly: () void }; declare function getSmallPet(): Fish | Bird; function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim ! undefined; }pet is Fish就是类型断言其形式是parameterName is Type其中parameterName必须是当前函数签名中的一个参数名。每当用某个变量调用isFish时TypeScript 会根据原始类型是否兼容把该变量缩小为Fish// “swim”和“fly”的调用现在都没问题。 let pet getSmallPet(); if (isFish(pet)) { pet.swim(); // pet: Fish } else { pet.fly(); // pet: BirdTypeScript 知道它一定不是 Fish }注意TypeScript 不仅在if分支中知道pet是Fish在else分支中也知道它不是Fish因此必然是Bird。类型断言最常见的进阶用法是配合Array.prototype.filter把Fish | Bird数组过滤成Fish数组——此时断言函数直接作为 filter 的回调即可type Fish { swim: () void; name: string }; type Bird { fly: () void; name: string }; declare function getSmallPet(): Fish | Bird; function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim ! undefined; } const zoo: (Fish | Bird)[] [getSmallPet(), getSmallPet(), getSmallPet()]; const underWater1: Fish[] zoo.filter(isFish); // 或者等价地 const underWater2: Fish[] zoo.filter(isFish) as Fish[]; // 对于更复杂的示例可能需要重复使用类型断言 const underWater3: Fish[] zoo.filter((pet): pet is Fish { if (pet.name sharkey) return false; return isFish(pet); });此外类的方法可以使用this is Type形式的断言来缩小调用者自身的类型。手册 classes.md 的“基于this的类型护卫”一节给出了完整示例FileSystemObject通过isFile(): this is FileRep、isDirectory(): this is Directory、isNetworked(): this is Networked this三个方法让调用方在if/else if链中逐步把FileSystemObject缩小为具体的子类或交叉类型// strictPropertyInitialization: false class FileSystemObject { isFile(): this is FileRep { return this instanceof FileRep; } isDirectory(): this is Directory { return this instanceof Directory; } isNetworked(): this is Networked this { return this.networked; } constructor(public path: string, private networked: boolean) {} } const fso: FileSystemObject new FileRep(foo/bar.txt, foo); if (fso.isFile()) { fso.content; // fso: FileRep } else if (fso.isDirectory()) { fso.children; // fso: Directory } else if (fso.isNetworked()) { fso.host; // fso: Networked FileSystemObject }十、断言函数Assertion Functions类型还可以通过断言函数来缩小。这是 TypeScript 3.7 引入的“断言签名”机制仓库的发布说明文档 typescript-3.7.md 有系统讲解。有一类函数专门在出现非预期结果时抛错如 Node.js 的assert。在 3.7 之前这类检查无法被 TypeScript 正确编码例如function yell(str) { assert(typeof str string); return str.toUppercase(); // 拼写错误但类型系统发现不了 }断言签名让编译器理解“若函数正常返回则条件必然为真”从而在后续代码中完成缩小function assert(condition: any, msg?: string): asserts condition { if (!condition) { throw new AssertionError(msg); } } function yell(str) { assert(typeof str string); return str.toUppercase(); // ~~~~~~~~~~~ // error: Property toUppercase does not exist on type string. // Did you mean toUpperCase? }asserts condition表示若assert成功返回则传入的condition必须为真否则应抛错。第二种断言签名asserts val is string与类型谓词签名val is string类似但不需要if包裹调用后变量立即被视作目标类型function assertIsString(val: any): asserts val is string { if (typeof val ! string) { throw new AssertionError(Not a string!); } } function yell(str: any) { assertIsString(str); // 现在 TypeScript 知道 str 是一个 string。 return str.toUppercase(); // ❌ 错误会被正确捕获 }断言签名可以表达更复杂的想法例如泛型的非空断言function assertIsDefinedT(val: T): asserts val is NonNullableT { if (val undefined || val null) { throw new AssertionError( Expected val to be defined, but received ${val} ); } }十一、可辨识联合类型Discriminated Unions前面讨论的多数例子都在缩小包含string、boolean、number等简单类型的单个变量。实际工程中更常处理结构化数据。假设要编码圆形与正方形圆有半径正方形有边长用kind字段区分形状。第一次尝试interface Shape { kind: circle | square; radius?: number; sideLength?: number; }用字符串字面量联合circle | square而不是string可以避免拼写错误——例如把kind写成rect会立刻报错错误码 2367// errors: 2367 function handleShape(shape: Shape) { if (shape.kind rect) { // ❌ 此比较条件似乎是无意的因为类型 circle | square 没有 rect 属性 } }但写getArea时问题来了radius是可选属性在strictNullChecks下直接访问会报错2532 / 18048。即便先检查kind circleTypeScript 依然无法把radius与kind关联起来// errors: 2532 18048 function getArea(shape: Shape) { if (shape.kind circle) { return Math.PI * shape.radius ** 2; // ❌ shape.radius 可能为 undefined } }非空断言shape.radius!能“说服”类型检查器但这种写法脆弱且不优雅function getArea(shape: Shape) { if (shape.kind circle) { return Math.PI * shape.radius! ** 2; } }问题的根源在于这种Shape编码让类型检查器无法根据kind推断radius/sideLength是否存在。正确做法是把信息传达给类型系统——将Shape拆成两个独立的接口让radius和sideLength各自成为必需属性interface Circle { kind: circle; radius: number; } interface Square { kind: square; sideLength: number; } type Shape Circle | Square;此时再直接访问shape.radius依然报错2339——因为Shape可能是Square而Square没有radius属性。但只要检查kind属性一切豁然开朗function getArea(shape: Shape) { if (shape.kind circle) { return Math.PI * shape.radius ** 2; // ✅ shape: Circle } }当联合类型的每个成员都包含一个类型为字面量类型的共同属性时TypeScript 把它视为可辨识联合discriminated union并据此排除联合成员。这里的kind就是Shape的辨识属性discriminant检查kind circle会排除所有kind不是circle的成员把shape缩小为Circle。相同的检查在switch语句中同样成立于是可以写出完全不需要!的完整getAreafunction getArea(shape: Shape) { switch (shape.kind) { case circle: return Math.PI * shape.radius ** 2; case square: return shape.sideLength ** 2; } }手册特意提示可以尝试删除switch里某些return关键字观察分支之间意外“掉落”时类型检查如何帮我们避错。可辨识联合不仅用于描述几何形状它适用于 JavaScript 中任何基于“消息方案”建模的场景——例如网络上的客户端/服务器通信消息或状态管理框架中编码的各类变更动作。十二、never类型与完备性检查缩小进行到最后联合类型的选项可能被减少到“一个都不剩”。此时 TypeScript 用never类型表示这种不应存在的状态。never的特性是可以赋值给任何类型但除了never本身之外没有类型可以赋值给它。利用这个特性可以做switch的完备性检查exhaustiveness checking。在getArea的switch里加一个default分支把shape赋值给nevertype Shape Circle | Square; function getArea(shape: Shape) { switch (shape.kind) { case circle: return Math.PI * shape.radius ** 2; case square: return shape.sideLength ** 2; default: const _exhaustiveCheck: never shape; return _exhaustiveCheck; } }只要所有成员都被处理default分支不可达赋值合法。一旦给Shape联合增加新成员例如Triangle编译器立刻报错错误码 2322——因为shape在default分支的类型是Triangle而Triangle不能赋值给never// errors: 2322 interface Triangle { kind: triangle; sideLength: number; } type Shape Circle | Square | Triangle; function getArea(shape: Shape) { switch (shape.kind) { case circle: return Math.PI * shape.radius ** 2; case square: return shape.sideLength ** 2; default: const _exhaustiveCheck: never shape; // ❌ 类型 Triangle 不可赋值给类型 never return _exhaustiveCheck; } }这个模式是“改一处类型定义编译器强制你同步更新所有处理逻辑”的经典实践特别适合在大型项目中维护协议与状态机代码。十三、与编译选项的联动strictNullChecks类型缩小与严格空值检查密不可分。仓库 compiler-options.md 对相关选项的定义如下选项类型默认值说明--strictNullChecksbooleanfalse在严格的null检查模式下null和undefined值不包含在任何类型里只允许用它们自己和any来赋值有个例外undefined可以赋值到void。--strictbooleanfalse启用所有严格检查选项包含--noImplicitAny、--noImplicitThis、--alwaysStrict、--strictBindCallApply、--strictNullChecks、--strictFunctionTypes和--strictPropertyInitialization。只有开启strictNullChecksnull/undefined才会作为显式的联合成员参与缩小关闭时可选属性会被假定为始终存在从而掩盖“访问可能未定义字段”的错误——这正是本文第十一节中Shape编码差异的底层原因。该选项同样可以通过tsconfig.json的extends机制在配置间覆盖参见 tsconfig.json.md 中configs/base.jsonstrictNullChecks: true与tsconfig.nostrictnull.jsonstrictNullChecks: false的示例。小结本文完整覆盖了手册 narrowing.md 的全部核心内容从typeof守卫、真值缩小、等式含 null巧用、in、instanceof六类内置缩小手段到赋值语句与控制流分析、类型断言与this is Type、断言函数、可辨识联合和never完备性检查。掌握它们之后绝大多数“TypeScript 居然还要写这种代码”的抱怨都会消失——因为类型系统已经能在你写普通 JavaScript 的同时把每个分支上的类型收得又准又严。继续深入可阅读本手册系列的其他章节函数进阶、对象类型、类以及进阶的类型操纵专题。赞分享文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载相关推荐TypeScript 类型收窄Narrowing全指南从 typeof 守卫到控制流分析TypeScript 类型收窄Narrowing全指南从 typeof 守卫到控制流分析 类型收窄Narrowing是 TypeScript 在条件分文档教程The Concise TypeScript Book 联合类型Union Type完全指南| 语法、类型收窄与可辨识联合实战The Concise TypeScript Book 联合类型Union Type完全指南 | 语法、类型收窄与可辨识联合实战 联合类型Union T文档教程claude-skills 项目 typescript-pro 技能指南TypeScript 类型守卫与类型窄化Type Guards and Narrowing实战claude skills 项目 typescript pro 技能指南TypeScript 类型守卫与类型窄化Type Guards and NarrowAI 技能AI 插件后端前端DevOps上一篇Go-Task 项目使用指南从基础到高级技巧下一篇Golang/dep 项目贡献指南详解创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表