今天会来说明 TypeScript 中内建 ReturnType
和 Parameters
的原始码(像是上图这样),如果你已经可以轻松看懂,欢迎直接左转去看我队友们的精彩文章!
一样让我们先来简单了解 ReturnType 和 Parameters 这两个 Utility Types 的使用。
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
的地方跳出错误提示,至於为什麽会这样,等等看原始码的时候就会了解了!
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]
。T3
会是 number[]
而不是 [number[]]
了。如果带入 <T>
的型别不是函式型别的子集合的话,则会得到 never
:
type T5 = Parameters<string>; // never
同样的,虽然有得到 T5
的型别是 never
,但 TypeScript 会在 string
的地方跳出错误提示,一样等等看原始码时就会知道为什麽:
如果直接看 ReturnType
这个 Utility Type 的原始码时,读者会看到一个先前没提到过的关键字 — infer
:
势必要先了解 infer
才能理解 ReturnType
的原始码,所以就先来看看这个 infer
怎麽用吧!
在前几天讲 Conditional Types 时笔者曾经提到 X extends Y ? T : F
中的 X extends Y
指的是「当 X 是 Y 的子集合」。但如果现在的 Y 并不是一个确切的型别,我们想要让 TypeScript 帮我们推导其型别的话,就可以用 infer
这个关键字。
写起来会像这样:
infer R
中的这个 R
就是 TypeScript 自己推导出来的型别,而且它是可以当 Conditional Type 的条件为 true
时,这个 R
是可以直接被拿来当成回传值使用的。
因为 infer
的概念比较抽象,透过实际范例会比较好理解,来我们先来看几个例子。
前几天在说明 Conditional Types 时,曾使用 Flatten
来做示范:
现在我们把 any[]
的部分修改成 (infer R)[]
,也就是 any
变成 infer R
(加上括号是为了让 TS 在解析语法时不会混淆):
这时候 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 的情况(即,:
後)在认识 infer
之後,让我们回头来看 ReturnType
和 Parameters
这两个 Utility Types 的原始码。
要看懂这两个 Utility Types 的原始码,前面几天提到的知识缺一不可:
infer
keywordReturnType
的原始码是:
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
在看他人写的 Utility Types 时,很重要的是做正确的断句,断好句後通常就会比较好理解它的意思,这里我们来帮它断句一下:
先看泛型的部分,也就是 <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
:
最後来看看当使用者带入的 T
满足函式型别时,回传的 R
是什麽。这个 R
就和今天认识的 infer
有关,在 Conditional Types 中的 T extends (...args: any) => infer R
,这里用了 infer R
来推论这个函式会回传的型别,并把这个函式会回传的型别取名为 R
,所以 R
指的就是函式会回传的型别。
最後来看看 Parameters<T>
,它的原始码是:
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
要看懂这段一样需要先试着将它们断句,读者们可以先自己试着练习看看。
断句後我们可以看到,前面 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
只能用在 Conditional Types 的 extends
後与 ?
前,不能用在 Generics Constraint 的 extends
後infer
推导出来的型别只能在 Conditional Types 中为 true 时被使用,不能在 false 是被使用infer
出来的型别是什麽,就回传出来看看
>>: Material UI in React [Day 24] Utils 工具组
本系列文章同步发布於笔者网站 前言 大家好,我是 Gene,如果有参与过 Cloud Native ...
第12 届iT邦帮忙铁人赛系列文章 (Day28) SignalR是实现即时通讯的框架,如下图,在S...
Button 是网页中最常用的元件,跟他相依的元件和情境也不少,因此虽然他不是一个介面,依然还是可...
此为番外,此篇选入番外的原因是 glob 并不是个工具,但是是个会常被各种工具采用的一种配置方式。...
●Java 自定义注解 创建自定义注解类似於编写接口,不同之处在於interface关键字以@符号为...