• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • TypeScript 全面进阶指南

    • 1.开篇:用正确的方式学习 TypeScript
    • 2.工欲善其事:打造最舒适的 TypeScript 开发环境
    • 3.进入类型的世界:理解原始类型与对象类型
    • 4.掌握字面量类型与枚举,让你的类型再精确一些
    • 5.函数与 Class 中的类型:详解函数重载与面向对象
    • 6.探秘内置类型:any、unknown、never 与类型断言
    • 7.类型编程好帮手:TypeScript 类型工具(上)
    • 8.类型编程好帮手:TypeScript 类型工具(下)
    • 9.类型编程基石:TypeScript 中无处不在的泛型
    • 10.结构化类型系统:类型兼容性判断的幕后
    • 11.类型系统层级:从 Top Type 到 Bottom Type
    • 12.类型里的逻辑运算:条件类型与 infer
    • 13.内置工具类型基础:别再妖魔化工具类型了!
    • 14.反方向类型推导:用好上下文相关类型
    • 15.数类型:协变与逆变的比较
    • 16.了解类型编程与类型体操的意义,找到平衡点
    • 17.内置工具类型进阶:类型编程进阶
    • 18.基础类型新成员:模板字符串类型入门
    • 19.类型编程新范式:模板字符串工具类型进阶
    • 20.工程层面的类型能力:类型声明、类型指令与命名空间
    • 21.在 React 中愉快地使用 TypeScript:内置类型与泛型坑位
    • 22.让 ESLint 来约束你的 TypeScript 代码:配置与规则集介绍
    • 23.全链路 TypeScript 工具库,找到适合你的工具
    • 24.说说 TypeScript 和 ECMAScript 之间那些事儿
    • 25.装饰器与反射元数据:了解装饰器基本原理与应用
    • 26.控制反转与依赖注入:基于装饰器的依赖注入实现
    • 27.TSConfig 全解(上):构建相关配置
    • 28.TSConfig 全解(下):检查相关、工程相关配置
    • 29.基于 Prisma + NestJs 的 Node API :前置知识储备
    • 30.基于 Prisma + NestJs 的 Node API :项目开发与基于 Heroku 部署
    • 31.玩转 TypeScript AST:AST Checker 与 CodeMod
    • 32.感谢相伴:是结束,也是开始
    • 33.漫谈篇:面试中的 TypeScript

14.反方向类型推导:用好上下文相关类型

TypeScript 拥有非常强大的类型推导能力,不仅会在你声明一个变量时自动推导其类型,也会基于函数内部逻辑自动推导其返回值类型,还会在你使用 typeof 、instanceof 等工具时自动地收窄类型(可辨识联合类型)等等。这些类型推导其实有一个共同点:它们的推导依赖开发者的输入,比如变量声明、函数逻辑、类型保护都需要开发者的输入。实际上, TypeScript 中还存在着另一种类型推导,它默默无闻却又无处不在,它就是这一节的主角:上下文类型(Contextual Typing) 。

这一节的内容比较短,因为上下文类型并不是一个多复杂、多庞大的概念(不涉及实现源码的情况下),但在实际开发中,我们经常会受益于上下文类型的推导能力,只不过你可能不知道背后是它得作用。学完这一节,以后感受到上下文类型存在时,你就可以在心里默默地说一句:“谢谢你,上下文类型”。

本节代码见:Contextual Typings

无处不在的上下文类型

首先举一个最常见的例子:

window.onerror = (event, source, line, col, err) => {}

在这个例子里,虽然我们并没有为 onerror 的各个参数声明类型,但是它们也已经获得了正确的类型。

当然你肯定能猜到,这是因为 onerror 的类型声明已经内置了:

interface Handler {
  // 简化
  onerror: OnErrorEventHandlerNonNull
}

interface OnErrorEventHandlerNonNull {
  (
    event: Event | string,
    source?: string,
    lineno?: number,
    colno?: number,
    error?: Error,
  ): any
}

我们自己实现一个函数签名,其实也是一样的效果:

type CustomHandler = (name: string, age: number) => boolean

// 也推导出了参数类型
const handler: CustomHandler = (arg1, arg2) => true

除了参数类型,返回值类型同样会纳入管控:

declare const struct: {
  handler: CustomHandler
}
// 不能将类型“void”分配给类型“boolean”。
struct.handler = (name, age) => {}

当然,不仅是箭头函数,函数表达式也是一样的效果,这里就不做展开了。

在这里,参数的类型基于其上下文类型中的参数类型位置来进行匹配,arg1 对应到 name ,所以是 string 类型,arg2 对应到 age,所以是 number 类型。这就是上下文类型的核心理念:基于位置的类型推导。同时,相对于我们上面提到的基于开发者输入进行的类型推导,上下文类型更像是反方向的类型推导,也就是基于已定义的类型来规范开发者的使用。

在上下文类型中,我们实现的表达式可以只使用更少的参数,而不能使用更多,这还是因为上下文类型基于位置的匹配,一旦参数个数超过定义的数量,那就没法进行匹配了。

// 正常
window.onerror = (event) => {}
// 报错
window.onerror = (event, source, line, col, err, extra) => {}

上下文类型也可以进行”嵌套“情况下的类型推导,如以下这个例子:

declare let func: (raw: number) => (input: string) => any

// raw → number
func = (raw) => {
  // input → string
  return (input) => {}
}

在某些情况下,上下文类型的推导能力也会失效,比如这里我们使用一个由函数类型组成的联合类型:

class Foo {
  foo!: number
}

class Bar extends Foo {
  bar!: number
}

let f1: { (input: Foo): void } | { (input: Bar): void }
// 参数“input”隐式具有“any”类型。
f1 = (input) => {} // y :any

我们预期的结果是 input 被推导为 Foo | Bar 类型,也就是所有符合结构的函数类型的参数,但却失败了。这是因为 TypeScript 中的上下文类型目前暂时不支持这一判断方式(而不是这不属于上下文类型的能力范畴)。

你可以直接使用一个联合类型参数的函数签名:

let f2: { (input: Foo | Bar): void }
// Foo | Bar
f2 = (input) => {} // y :any

而如果联合类型中将这两个类型再嵌套一层,此时上下文类型反而正常了:

let f3:
  | { (raw: number): (input: Foo) => void }
  | { (raw: number): (input: Bar) => void }

// raw → number
f3 = (raw) => {
  // input → Bar
  return (input) => {}
}

这里被推导为 Bar 的原因,其实还和我们此前了解的协变、逆变有关。任何接收 Foo 类型参数的地方,都可以接收一个 Bar 类型参数,因此推导到 Bar 类型要更加安全。

void 返回值类型下的特殊情况

我们前面说到,上下文类型同样会推导并约束函数的返回值类型,但存在这么个特殊的情况,当内置函数类型的返回值类型为 void 时:

type CustomHandler = (name: string, age: number) => void

const handler1: CustomHandler = (name, age) => true
const handler2: CustomHandler = (name, age) => 'linbudu'
const handler3: CustomHandler = (name, age) => null
const handler4: CustomHandler = (name, age) => undefined

你会发现这个时候,我们的函数实现返回值类型变成了五花八门的样子,而且还都不会报错?同样的,这也是一条世界底层的规则,上下文类型对于 void 返回值类型的函数,并不会真的要求它啥都不能返回。然而,虽然这些函数实现可以返回任意类型的值,但对于调用结果的类型,仍然是 void:

const result1 = handler1('linbudu', 599) // void
const result2 = handler2('linbudu', 599) // void
const result3 = handler3('linbudu', 599) // void
const result4 = handler4('linbudu', 599) // void

看起来这是一种很奇怪的、错误的行为,但实际上,我们日常开发中的很多代码都需要这一“不正确的”行为才不会报错,比如以下这个例子:

const arr: number[] = []
const list: number[] = [1, 2, 3]

list.forEach((item) => arr.push(item))

这是我们常用的简写方式,然而,push 方法的返回值是一个 number 类型(push 后数组的长度),而 forEach 的上下文类型声明中要求返回值是 void 类型。如果此时 void 类型真的不允许任何返回值,那这里我们就需要多套一个代码块才能确保类型符合了。

但这真的是有必要的吗?对于一个 void 类型的函数,我们真的会去消费它的返回值吗?既然不会,那么它想返回什么,全凭它乐意就好了。我们还可以用另一种方式来描述这个概念:你可以**将返回值非 void 类型的函数(**​ ​() => list.push()​​ )作为返回值类型为 void 类型(​​arr.forEach​​ )的函数类型参数。

总结与预告

在这一节里,我们学习了上下文类型这“另一个方向”的类型推导,了解了它是基于位置进行类型匹配的,以及上下文类型中 void 类型返回值的特殊情况。

这一节比较轻松对吧?那在下一节,我们会学习一个稍微复杂点的概念:函数类型兼容性比较,以及其中的协变与逆变概念。我们在前面类型层级一节中,并没有提及函数类型地比较,这也是因为其中的概念相对复杂,需要更多的前置知识与更多的消化过程,因此我单独准备了一节内容。

扩展阅读

将更少参数的函数赋值给具有更多参数的函数类型

在上面的例子中,我们看到了这么一段代码:

const arr: number[] = []
const list: number[] = [1, 2, 3]

list.forEach((item) => arr.push(item))

在 forEach 的函数中,我们会消费 list 的每一个成员。但我们有时也会遇到并不实际消费数组成员的情况:

list.forEach(() => arr.push(otherFactory))

这个时候,我们实际上就是在将更少参数的函数赋值给具有更多参数的函数类型!

再看一个更明显的例子:

function handler(arg: string) {
  console.log(arg)
}

function useHandler(callback: (arg1: string, arg2: number) => void) {
  callback('linbudu', 599)
}

useHandler(handler)

handler 函数的类型签名很明显与 useHandler 函数的 callback 类型签名并不一致,但这里却没有报错。从实用意义的角度来看,如果我们需要类型签名完全一致,那么就需要为 handler 再声明一个额外的对应到 arg2 的参数,然而我们的 handler 代码里实际上并没有去消费第二个参数。这实际上在 JavaScript 中也是我们经常使用的方式:即使用更少入参的函数来作为一个预期更多入参函数参数的实现。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
13.内置工具类型基础:别再妖魔化工具类型了!
Next
15.数类型:协变与逆变的比较