拓冰建站拓冰建站
首页 / 资讯中心 / 正文

TypeScript 类型推断深入指南:从变量定义到函数赋值、解构与 noImplicitAny(typescript-book-chinese)

文档教程【免费下载链接】typescript-book-chineseTypeScript Deep Dive 中文版项目地址https://gitcode.com/gh_mirrors/ty/typescript-book-chinese点击查看免费下载类型推断Type Inference是 TypeScript 静态类型系统的核心机制之一在绝大多数情况下你无需显式书写类型注解编译器会根据一些简单而一致的规则自动算出变量、函数返回值和复杂结构的类型。本文以《深入理解 TypeScript》typescript-book-chinese 仓库中的 docs/typings/typeInference.md 为骨架结合仓库内类型保护、类型兼容性、泛型与infer等姊妹章节系统讲解类型推断的七大场景、类型流动方向的底层心智模型以及noImplicitAny等编译器选项对推断行为的约束。读完本文你将能够熟练地预测TypeScript 在何时会推断出什么类型、何时会报错从而写出无需冗余注解、类型却依然安全的代码。类型推断的本质从初始化表达式推导类型TypeScript 能根据一些简单的规则推断检查变量的类型这些规则可以通过实践很快掌握。其中最重要的一条是变量的类型由定义初始化表达式推断。let foo 123; // foo 是 number let bar hello; // bar 是 string foo bar; // Error: 不能将 string 赋值给 number这里的关键在于foo的类型在声明的那一刻就被123这个字面量锁定为number此后任何与number不兼容的赋值都会在编译期被拦截。从信息流向的角度看这是一个从右向左流动类型的示例类型信息从等号右侧的初始化表达式流向左侧的变量声明。理解流向这个概念很重要它是后续所有推断规则的心智模型基础——类型推断并不是随意的猜测而是沿着 JavaScript 代码天然的赋值与求值结构把类型信息从一个位置传递到另一个位置。函数返回类型从 return 语句向上推断函数的返回类型能被return语句推断出来。例如下面的add函数虽然没有任何显式返回类型注解TypeScript 依然能推断出它返回一个数字function add(a: number, b: number) { return a b; }由于a、b都是numbera b的结果自然是number因此add的返回类型被推断为number。从流向的角度看这是一个从底部流出类型的例子类型信息从函数体底部的return语句向上流出汇聚为函数的签名。当你在其他地方调用add并试图把结果当作非number类型使用时编译器会依据这个推断出的返回类型给出错误。值得注意的是返回类型推断在一般情况下是可靠的但在某些特殊场景如函数内部调用了隐式any的函数下会产生污染这一点我们会在后文小心使用返回值一节专门讨论。赋值驱动的推断函数类型与回调参数函数参数类型和返回值也能通过赋值来推断。看下面的例子foo被注解为Adder类型而Adder是一个(a: number, b: number) number的函数类型因此foo的参数a、b会被推断为numbertype Adder (a: number, b: number) number; let foo: Adder (a, b) a b;这个事实可以用下面的代码来证明——TypeScript 会发出正如你期望的错误警告type Adder (a: number, b: number) number; let foo: Adder (a, b) { a hello; // Error不能把 string 类型赋值给 number 类型 return a b; };这里类型信息从左侧的类型注解流向右侧的函数实现是一个从左向右流动类型的示例。函数实现中参数的空位没有注解的a、b会被左侧声明的函数类型签名填充。更进一步如果你创建一个函数并且函数参数是一个回调函数相同的赋值规则同样适用——从argument实参到parameter形参只是变量赋值的另一种形式type Adder (a: number, b: number) number; function iTakeAnAdder(adder: Adder) { return adder(1, 2); } iTakeAnAdder((a, b) { a hello; // Error: 不能把 string 类型赋值给 number 类型 return a b; });在这个例子中传给iTakeAnAdder的匿名回调虽然没有标注任何参数类型但编译器根据adder: Adder的形参类型把回调的a、b推断为number并因此在a hello处报错。这正是 JavaScript 中最常见的回调参数自动推断模式——也是Array.prototype.map、forEach等 API 回调能自动获得元素类型的原因。从源码视角看回调推断的边界回调推断并不是无限的。在 docs/typings/typeCompatibility.md 中可以看到两个函数相互赋值时TypeScript 对返回类型采用协变Covariant比较——返回类型必须包含足够的数据对参数数量则允许更少的参数函数能够选择性地忽略多余的参数const iTakeSomethingAndPassItAnErr (x: (err: Error, data: any) void) { /* 做一些其他的 */ }; iTakeSomethingAndPassItAnErr(() null); // ok iTakeSomethingAndPassItAnErr(err null); // ok iTakeSomethingAndPassItAnErr((err, data) null); // ok // Error: 参数类型 (err: any, data: any, more: any) null 不能赋值给参数类型 (err: Error, data: any) void iTakeSomethingAndPassItAnErr((err, data, more) null);这意味着当你为一个回调函数提供形参位置时编译器会尽量根据目标函数类型去推断每个参数的类型但如果实参的参数数量超过形参推断就会失败并报错。这也是类型可以从左向右流动但不能凭空多出信息的体现。结构化数据推断对象字面量与数组这些简单的推断规则同样适用于结构化的存在对象字面量。例如下面这种情况下foo的类型被推断为{ a: number, b: number }const foo { a: 123, b: 456 }; foo.a hello; // Error不能把 string 类型赋值给 number 类型数组也一样const bar [1, 2, 3]; bar[0] hello; // Error不能把 string 类型赋值给 number 类型编译器对对象字面量和数组元素逐个执行推断a、b由123、456推断为number数组bar的元素由[1, 2, 3]推断为number。此后对结构内部任何成员的类型外赋值都会被拦截。结构推断与 Freshness新鲜度检查的关系需要补充的是当对象字面量被直接赋值给一个带注解的变量或函数参数时TypeScript 还会启用更严格的对象字面量检查Freshness。正如 docs/typings/freshness.md 所展示的function logName(something: { name: string }) { console.log(something.name); } logName({ name: matt }); // ok logName({ name: matt, job: being awesome }); // Error: 对象字面量只能指定已知属性job 属性在这里并不存在。注意这种只允许已知属性的错误提示只会发生在对象字面量上。如果你先把对象存入一个变量再传入则由于结构类型Structural Typing的存在多出的属性是被允许的见 docs/typings/typeCompatibility.md 中iTakePoint2D(point3D); // 额外的信息没关系的例子。这一差异在实战中非常常见也是初学者最容易困惑的地方。解构中的类型推断这些推断规则也适用于**解构Destructuring**中。先看对象解构const foo { a: 123, b: 456 }; let { a } foo; a hello; // Error不能把 string 类型赋值给 number 类型再看数组解构const bar [1, 2]; let [a, b] bar; a hello; // Error不能把 string 类型赋值给 number 类型编译器会沿着解构的模式把源对象/数组的成员类型一一传递给解构出来的新变量。更进一步如果函数参数能够被推断出来那么解构亦是如此。在如下例子中函数参数能够被解构为a/b成员并且它们的类型会从形参类型自动推断type Adder (number: { a: number; b: number }) number; function iTakeAnAdder(adder: Adder) { return adder({ a: 1, b: 2 }); } iTakeAnAdder(({ a, b }) { // a, b 的类型能被推断出来 a hello; // Error不能把 string 类型赋值给 number 类型 return a b; });可以看到({ a, b }) ...中的a、b被自动推断为number因为它们是从形参类型{ a: number; b: number }中解构出来的。这一特性让 React 组件、Redux reducer 等重度使用解构参数的场景获得了完全的类型安全而无需任何参数注解。类型保护块内变量的另一种推断形式在前面章节 docs/typings/typeGuard.md类型保护中我们已经知道它如何帮助我们改变和缩小类型范围特别是在联合类型下。从推断的角度看类型保护只是一个块中变量的另一种推断形式——当条件成立时TypeScript 会重新推断该变量在条件块内的更窄类型。例如typeof运算符在条件块中会收窄联合类型function doSome(x: number | string) { if (typeof x string) { // 在这个块中TypeScript 知道 x 的类型必须是 string console.log(x.subtr(1)); // Error: subtr 方法并没有存在于 string 上 console.log(x.substr(1)); // ok } x.substr(1); // Error: 无法保证 x 是 string 类型 }instanceof与else分支的收窄同理class Foo { foo 123; } class Bar { bar 123; } function doStuff(arg: Foo | Bar) { if (arg instanceof Foo) { console.log(arg.foo); // ok console.log(arg.bar); // Error } else { // 这个块中一定是 Bar console.log(arg.foo); // Error console.log(arg.bar); // ok } }此外TypeScript 还支持基于in操作符、字面量类型如kind: foo判别联合以及用户自定义的类型保护函数arg is SomeType返回值形式的推断收窄详见 docs/typings/typeGuard.md。无论是哪种形式其本质都是在特定代码块内编译器用更精确的类型覆盖了变量原本的宽泛类型这正是推断机制在控制流层面的延伸。警告与陷阱尽管类型推断在多数情况下正确且方便但存在两个常见陷阱需要特别小心。小心使用参数无注解时类型不会流入函数参数如果类型不能被赋值推断出来类型也将不会流入函数参数中。例如下面这个例子编译器并不知道foo的类型所以它也就不能推断出a或者b的类型const foo (a, b) { /* do something */ };此时a、b都是隐式的any——TypeScript 放弃了推断把类型控制权完全交给运行时。然而如果foo添加了类型注解函数参数也就能被推断a、b都能被推断为number类型type TwoNumberFunction (a: number, b: number) void; const foo: TwoNumberFunction (a, b) { /* do something */ };这正是前面从左向右流动规则的直接推论参数类型推断必须有一个锚点类型注解或上下文类型否则推断无从谈起。小心使用返回值隐式 any 的污染尽管 TypeScript 一般情况下能推断函数的返回值但它可能并不是你想要的。例如如下的foo函数它的返回值为anyfunction foo(a: number, b: number) { return a addOne(b); } // 一些使用 JavaScript 库的特殊函数 function addOne(a) { return a 1; }这是因为返回值的类型被一个缺少类型定义的addOne函数所影响addOne的参数a是any所以addOne返回值为any进而a addOne(b)的结果也是any最终foo的返回值被推断为any。一个隐式any的脏类型会沿调用链向上传播让本可精确推断的返回类型退化为any从而失去类型保护。提示我发现最简单的方式是明确的写上函数返回值毕竟这些注解是一个定理而函数是注解的一个证据。显式标注返回类型后编译器会在函数体内部以该类型为基准进行校验addOne返回any的污染就会被隔离在foo内部不会扩散到foo的调用方。noImplicitAny让隐式 any 无处遁形这里还有一些其他可以想象的情景但有一个好消息是有编译器选项noImplicitAny可以捕获这些 bug。选项noImplicitAny用来告诉编译器当无法推断一个变量时发出一个错误或者只能推断为一个隐式的any类型。此时你有两种出路通过显式添加:any的类型注解来让它成为一个any类型通过一些更正确的类型注解来帮助 TypeScript 推断类型。在实际的 tsconfig.json 中该选项通常与严格模式一起开启。仓库中的 docs/project/compilationContext.md 给出了完整的编译上下文配置说明其中严格的类型检查选项一栏明确注释了它的含义/* 严格的类型检查选项 */ strict: true, // 启用所有严格类型检查选项 noImplicitAny: true, // 在表达式和声明上有隐含的 any类型时报错 strictNullChecks: true, // 启用严格的 null 检查 noImplicitThis: true, // 当 this 表达式值为 any 类型的时候生成一个错误 alwaysStrict: true, // 以严格模式检查每个模块并在每个文件里加入 use strict推荐做法是在新项目中直接开启strict: true它内部隐含了noImplicitAny、strictNullChecks、noImplicitThis等全部严格选项这样参数无注解即报错的约束会从第一天起生效把隐式any的隐患扼杀在编译期。这一点也在 docs/faqs/type-system-behavior.md 中得到印证为了避免相关问题需要开启noImplicitAny选项当检测到有任何参数的类型为any时它将会发出一个警告。延伸推断如何与泛型、infer 协同类型推断并不局限于字面量初始化这一类简单场景它与泛型的结合才是类型系统真正强大的地方。泛型推断从实参推导类型参数在 docs/typings/generices.md 中可以看到泛型函数调用时类型参数会根据实参自动推断并反过来约束返回值function reverseT(items: T[]): T[] { const toreturn []; for (let i items.length - 1; i 0; i--) { toreturn.push(items[i]); } return toreturn; } const sample [1, 2, 3]; let reversed reverse(sample); reversed[0] 1; // Error reversed [1, 2]; // Error reversed[0] 1; // ok reversed [1, 2]; // okreverse(sample)中的sample是number[]TypeScript 据此把T推断为number于是返回值reversed被推断为number[]——任何混入string的赋值都会被拒绝。另一个典型的实战例子是getJSONT这种返回 Promise的封装。只要调用时传入泛型参数返回值类型就能被完整推断出来从而省去手动注解const getJSON T(config: { url: string; headers?: { [key: string]: string } }): PromiseT { const fetchConfig { method: GET, Accept: application/json, Content-Type: application/json, ...(config.headers || {}) }; return fetch(config.url, fetchConfig).thenT(response response.json()); }; type LoadUserResponse { user: { name: string; email: string; }[]; }; function loaderUser() { return getJSONLoadUserResponse({ url: https://example.com/users }); }注意如果你使用泛型仅用于单个参数位置而没有在成员之间提供约束那它通常是一种误用——例如declare function fooT(arg: T): void;并不比declare function foo(arg: any): void更安全见 docs/typings/generices.md 中误用的泛型一节。推断的价值在于约束关系而不在于形式上的泛型化。infer在条件类型中反向推断类型推断还有更进阶的形态infer关键字。在 docs/tips/infer.md 中它被定义为在extends条件语句中待推断的类型变量。看这个最简单的示例type ParamTypeT T extends (arg: infer P) any ? P : T;含义是如果T能赋值给(arg: infer P) any则结果是该函数类型中的参数P否则返回Tinterface User { name: string; age: number; } type Func (user: User) void; type Param ParamTypeFunc; // Param User type AA ParamTypestring; // stringTypeScript 2.8 起内置了一系列基于infer的工具类型例如提取函数返回值的ReturnTypeT、提取构造函数参数的ConstructorParametersT与提取实例类型的InstanceTypeTtype ReturnTypeT T extends (...args: any[]) infer P ? P : any; type Func () User; type Test ReturnTypeFunc; // Test Userinfer还可以配合Distributive conditional types玩出更多花样例如 tuple 转 union[string, number]→string | numbertype ElementOfT T extends Arrayinfer E ? E : never; type TTuple [string, number]; type ToUnion ElementOfTTuple; // string | number以及利用逆变位置上同一类型变量的多个候选会被推断为交叉类型的特性实现 union 转 intersectionT1 | T2→T1 T2type UnionToIntersectionU (U extends any ? (k: U) void : never) extends ((k: infer I) void) ? I : never; type Result UnionToIntersectionT1 | T2; // T1 T2完整的推导过程与更多用例见 docs/tips/infer.md。理解infer之后你会意识到类型推断并非只能被动地观察字面量——你完全可以主动设计推断让类型系统替你从已有结构中提取出新的类型。小结一张类型推断速查表场景推断来源类型流向示例文档章节变量定义初始化表达式右 → 左let foo 123推断为number函数返回类型return语句底部 → 顶部add(a, b) { return a b }推断为number函数赋值左侧类型注解左 → 右let foo: Adder (a, b) ...回调参数形参的函数类型实参 → 形参iTakeAnAdder((a, b) ...)对象/数组字面量成员初始值成员级逐个推断{ a: 123 }→{ a: number }解构源对象/数组成员类型结构 → 局部变量let { a } foo类型保护块收窄条件控制流重推断typeof x string块内x: string泛型调用实参类型实参 → 类型参数 → 返回值reverse(sample)→T numberinfer条件类型结构匹配条件类型内部提取ReturnTypeT、ElementOfT掌握这些规则配合noImplicitAny推荐直接启用strict: true的编译期约束你就能在享受少写注解便利的同时始终获得可靠的类型安全。相关扩展阅读推荐仓库内 docs/typings/typeGuard.md类型保护、docs/typings/typeCompatibility.md类型兼容性、docs/typings/generices.md泛型与 docs/project/compilationContext.md编译上下文与 tsconfig 完整配置。赞分享文档教程【免费下载链接】typescript-book-chineseTypeScript Deep Dive 中文版项目地址https://gitcode.com/gh_mirrors/ty/typescript-book-chinese点击查看免费下载相关推荐TypeScript 函数返回类型推断实战从返回值自动推导类型The Concise TypeScript BookTypeScript 函数返回类型推断实战从返回值自动推导类型The Concise TypeScript Book 函数返回类型推断Type from文档教程The Concise TypeScript Book深入理解 Type from Value从值推断类型The Concise TypeScript Book深入理解 Type from Value从值推断类型 在《The Concise TypeScr文档教程TypeScript 函数返回类型推断详解《The Concise TypeScript Book》函数返回类型推断精讲TypeScript 函数返回类型推断详解《The Concise TypeScript Book》函数返回类型推断精讲 《The Concise TypeS文档教程上一篇10分钟上手企业级后台框架renren-ui从搭建到自定义组件全攻略下一篇【热门开源项目下载】wallpaper-box 桌面壁纸客户端完全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门