[Day10] TS:什麽!Conditional Types 中还能建立型别?使用 infer 来实作 ReturnType 和 Parameters

carbon

今天会来说明 TypeScript 中内建 ReturnTypeParameters 的原始码(像是上图这样),如果你已经可以轻松看懂,欢迎直接左转去看我队友们的精彩文章!

ReturnType 和 Parameters 的使用

一样让我们先来简单了解 ReturnTypeParameters 这两个 Utility Types 的使用。

ReturnType

ReturnType<T> 是 TypeScript 内建的 Utility Type,它能够接受一个参数 T,这个参数满足「函式型别」的话(即,T 要是函式型别的子集合),则会回传这个函式「回传值的型别(return type)」;否则,就会回传 any

来看一下官网提供的几个范例:

type T1 = ReturnType<() => string>; // string

type T2 = ReturnType<(s: string) => number[]>; // number[]
  • 第一个范例中,ReturnType<T><> 中带入的是 () => string 这个函式型别,因为这个函式会回传 string,所以 T1 会是 string
  • 第二个范例中,因为 (s: string) => number[] 会回传 number[],所以 T1 会是 number[]

如果带入的型别不符合,则会回传 any

type T3 = ReturnType<string>; // any

有个稍微特别的地方是,虽然 T3 的型别会是 any,但 TypeScript 会在 string 的地方跳出错误提示,至於为什麽会这样,等等看原始码的时候就会了解了!

ReturnType

Parameters

Parameters<T> 则是 TypeScript 内建的另一个 Utility Type,它能够接受一个参数 T,这个参数满足「函式型别」的话(即,T 要是函式型别的子集合),则会以「 tuple type 来回传函式的「参数(parameters)」,否则会回传 never

一样来看看几个例子,这四个范例带进去 T 的型别都能满足函式型别:

type T1 = Parameters<(a: number, b: string) => number>; // [a: number, b: string]

type T2 = Parameters<(a: number[]) => number>; // [a: number[]]

type T3 = Parameters<(a: { firstName: string; lastName: string }) => string>; // [a: { firstName: string; lastName: string; }]

type T4 = Parameters<(...a: number[]) => number>; // number[]
  • 在第一个范例中,因为带入 <T> 内的 (a: number, b: string) => number 是函式型别的子集合,所以 Parameters 这个 Utility Type 就会把函式的参数用 tuple type 的方式回传出来,因此 T1 会是 [a: number, b: string]
  • 第二和第三个范例也是一样的意思,只是分别带入的参数型别是阵列和物件而已。
  • 第四个范例比较特别一点,读者需要先了解 JavaScript 中可以使用 rest parameters 来取得所有函式参数的内容,如此就可以理解为什麽它 T3 会是 number[] 而不是 [number[]] 了。

如果带入 <T> 的型别不是函式型别的子集合的话,则会得到 never

type T5 = Parameters<string>; // never

同样的,虽然有得到 T5 的型别是 never,但 TypeScript 会在 string 的地方跳出错误提示,一样等等看原始码时就会知道为什麽:

Parameters

认识 Conditional Types 中的 infer

如果直接看 ReturnType 这个 Utility Type 的原始码时,读者会看到一个先前没提到过的关键字 — infer

infer

势必要先了解 infer 才能理解 ReturnType 的原始码,所以就先来看看这个 infer 怎麽用吧!

在前几天讲 Conditional Types 时笔者曾经提到 X extends Y ? T : F 中的 X extends Y 指的是「当 X 是 Y 的子集合」。但如果现在的 Y 并不是一个确切的型别,我们想要让 TypeScript 帮我们推导其型别的话,就可以用 infer 这个关键字。

写起来会像这样:

infer

infer R 中的这个 R 就是 TypeScript 自己推导出来的型别,而且它是可以当 Conditional Type 的条件为 true 时,这个 R 是可以直接被拿来当成回传值使用的。

因为 infer 的概念比较抽象,透过实际范例会比较好理解,来我们先来看几个例子。

范例一

前几天在说明 Conditional Types 时,曾使用 Flatten 来做示范:

Conditional Types

现在我们把 any[] 的部分修改成 (infer R)[],也就是 any 变成 infer R(加上括号是为了让 TS 在解析语法时不会混淆):

infer

这时候 TS 就会根据使用者带入的型别,自动推导这个 R 应该是什麽型别。同时,被推导出来的 R 还可以在条件为 true 时作为回传值使用。

读者可以猜想下面的 R 会是什麽呢?

type Flatten<T> = T extends (infer R)[] ? R : T;

type T1 = Flatten<number[]>; // number,且 R 会是 number

type T2 = Flatten<(string | number)[]>; // string | number,且 R 会是 string | number

type T3 = Flatten<number>; // number

如果 T 是阵列型别的子集合的话,则会回传这个被推导出来的 R,否则直接回传 T

  • 第一个例子中,因为带入 <> 的是 number[],所以 R 会被推论是 number,因此 T1 就会是 number
  • 第二个例子中,因为带入 <> 的是 (string | number)[],所以 R 会被推论是 string | number,因此 T2 就会是 string | number
  • 第三个例子中,因为带入 <> 的是 number,而 number 并不是阵列型别的子集合,所以会直接回传原本带入 <> 的型别。

例子二

让我们再看另一个例子:

type InferResp<T> = T extends { response: infer R; status: number } ? R : T;

在这个 Utility Type 中,T 如果是 { response: ...; status: number } 的子集合,则会回传 R,否则会回传 T,但这里并不清楚 response 的型别是什麽,所以使用 { response: infer R, ... } 来让 TS 推论:

type T1 = InferResp<{ response: { data: 'foobar' }; status: 200 }>; // { data: 'foobar' }

type T2 = InferResp<{ status: 400 }>; // { status: 400 }

在上面的例子中可以看到,当带入的 T 满足 { response: ...; status: number } 时,R 就可以自动被推导成 {data: 'foobar'}。但如果不满足的话,就会直接回传原本带入的型别 { status: 400 }

从上面的例子中可以看到,infer 适合用在需要做条件判断,但型别又不完全明确时使用

这个 infer 很特别,需要多感觉一下,等等说明 ReturnType 的原始码时可以再体会看看。有几个使用 infer 时一定要留意的细节:

  • 关键字 infer 只能在 Conditional Types 中的 extends 被使用(更确切来说是 extends 後且 ? 前),不能在限制泛型(Generics Constraint)中的 extends 使用。
  • 使用 infer R 後,这个被推导出来的型别 R 虽然能够被当成型别直接回传,但它只能用在符合 True 的条件使用(即,? 後且 : 前),不能用在 False 的情况(即,: 後)

理解 ReturnType 和 Parameters 的实作

在认识 infer 之後,让我们回头来看 ReturnTypeParameters 这两个 Utility Types 的原始码。

要看懂这两个 Utility Types 的原始码,前面几天提到的知识缺一不可:

ReturnType

ReturnType 的原始码是:

type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;

在看他人写的 Utility Types 时,很重要的是做正确的断句,断好句後通常就会比较好理解它的意思,这里我们来帮它断句一下:

ReturnType

先看泛型的部分,也就是 <T extends (...args: any) => any>,这里面同时有两个 > 在内,一开始会让人容易有点混淆,但读者只要知道 (...args: any) => any 这个是 TypeScript 的 Function Type,意思就是这个函式可以接受任何型别作为参数,也可回传任何型别的值。

根据前几天对於 Generics Constraint 的说明,将可以理解 <T extends (...args: any) => any> 完整的意思就是说,T 需要是 (...args: any) => any 的子集合,也就是说 T 需要满足函式型别,不论这个函式的参数和回传值的型别是什麽都可以。

现在读者应该可以知道,为什麽在刚刚的范例中使用 ReturnType<string> 时,TS 会回报错误提示了,这是因为 string 并不是函式型别的子集合。

接着把注意力放到等号後的 Conditional Types,T extends (...args: any) => infer R ? R : any;,根据前几天对 Conditional Types 的说明,读者应该可以知道,这里的判断式 T extends (...args: any) => infer R,如果这个判断式为真,就会回传 R,否则会回传 any

看到这里读者应该可以理解,为什麽在刚刚的范例中使用 ReturnType<string> 时,虽然 TS 有报错,但最终还是可以得到 any 的型别。因为如果带入的 T 不满足函式型别的话,就会得到 any

ReturnType

最後来看看当使用者带入的 T 满足函式型别时,回传的 R 是什麽。这个 R 就和今天认识的 infer 有关,在 Conditional Types 中的 T extends (...args: any) => infer R,这里用了 infer R 来推论这个函式会回传的型别,并把这个函式会回传的型别取名为 R,所以 R 指的就是函式会回传的型别。

Parameters

最後来看看 Parameters<T>,它的原始码是:

type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

要看懂这段一样需要先试着将它们断句,读者们可以先自己试着练习看看。

Parameters

断句後我们可以看到,前面 Generic Constraints 的部分是 <T extends (...args: any) => any>,需要满足的限制是 (...args: any) => any,这个部分和 ReturnType 是一样的,也就是只要符合函式型别即可,不论该函式的参数或回传值的型别是什麽。

在等号後的 Conditional Types 中可以看到如果 T 是函式型别的子集合(即,T extends (...args: infer P) => any),就会回传 P,否则会回传 never

那麽这个 P 是什麽呢?从 (...args: infer P) 可以看出这个 P 是透过 infer 来推论带入 <> 中函式的「参数的型别」,推论後命名为 P,并作为回传值使用。

范例程序码

https://tsplay.dev/WYY9rW @ TypeScript Playground

infer 学习重点

  • 关键字 infer 只能用在 Conditional Types 的 extends 後与 ? 前,不能用在 Generics Constraint 的 extends
  • 透过 infer 推导出来的型别只能在 Conditional Types 中为 true 时被使用,不能在 false 是被使用
  • 如果未来碰到比较复杂的 Utility Types,不知道 infer 出来的型别是什麽,就回传出来看看

参考资料


<<:  011-快捷键

>>:  Material UI in React [Day 24] Utils 工具组

云端定义 1

本系列文章同步发布於笔者网站 前言 大家好,我是 Gene,如果有参与过 Cloud Native ...

制作婚礼现场即时留言版- Azure SignalR Service I

第12 届iT邦帮忙铁人赛系列文章 (Day28) SignalR是实现即时通讯的框架,如下图,在S...

Day 18 - UML x Component — Button

Button 是网页中最常用的元件,跟他相依的元件和情境也不少,因此虽然他不是一个介面,依然还是可...

Extra01 - glob - 配置目标档案与目录

此为番外,此篇选入番外的原因是 glob 并不是个工具,但是是个会常被各种工具采用的一种配置方式。...

Day28 Java 注解

●Java 自定义注解 创建自定义注解类似於编写接口,不同之处在於interface关键字以@符号为...