泛型是 TypeScript(如下简称 TS) 比较高级的功能之一,理解起来也比较困难。泛型应用场景很是普遍,不少地方都能看到它的影子。平时咱们阅读开源 TS 项目源码,或者在本身的 TS 项目中使用一些第三方库(好比 React)的时候,常常会看到各类泛型定义。若是你不是特别了解泛型,那么你极可能不只不会用,不会实现,甚至看不懂这是在干什么。html
相信你们都经历过,看到过,或者正在写「一些应用,这些应用充斥着各类重复类型定义, any 类型层出不穷,鼠标移到变量上面的提示只有 any,不要说类型操做了,类型能写对都是个问题」。我也经历过这样的阶段,那个时候我对 TS 还比较陌生。前端
随着在 TS 方面学习的深刻,愈来愈认识到 「真正的 TS 高手都是在玩类型」,对类型进行各类运算生成新的类型。这也好理解,毕竟 「TS 提供的其实就是类型系统」。你去看那些 TS 高手的代码,会各类「花式使用泛型」。 能够说泛型是一道坎,只有真正掌握它,你才知道「原来 TS 还能够这么玩」。怪不得面试的时候你们都愿意问泛型,尽管面试官极可能也不怎么懂。react
「只有理解事物的内在逻辑,才算真正掌握了,否则永远只是皮毛,不得其法」。 本文就带你走进泛型,带你从另外一个角度看看究竟什么是泛型,为何要有它,它给 TS 带来了什么样的不一样。git
❝注意:不一样语言泛型略有不一样,知识迁移虽然能够,可是不能生搬硬套,本文所讲的泛型都指的是 TS 下的泛型。程序员
❞
我总结了一下,学习 TS 有两个难点。第一个是TS 和 JS 中容易混淆的写法
,第二个是TS中特有的一些东西
。github
好比:web
(容易混淆的箭头函数)面试
再好比:typescript
(容易混淆的 interface 内的小括号)编程
好比 typeof,keyof, infer 以及本文要讲的泛型。
「把这些和 JS 中容易混淆的东西分清楚,而后搞懂 TS 特有的东西,尤为是泛型」(其余基本上相对简单),TS 就入门了。
在强类型语言中,通常而言须要给变量指定类型才能使用该变量。以下代码:
const name: string = "lucifer";
console.log(name); 复制代码
咱们须要给 name 声明 string 类型,而后才能在后面使用 name 变量,当咱们执行如下操做的时候会报错。
divide(1, name)
,其中 divide 就是功能就是
将第一个数(number 类型)除以第二个数(number 类型),并将结果返回
。
TS 除了提供一些基本类型(好比上面的 string)供咱们直接使用。还:
inteface
和
type
关键字供咱们定义本身的类型,以后就能像使用基本类型同样使用本身定义的类型了。
也就是说泛型也是一种类型,只不过不一样于 string, number 等具体的类型,它是一种抽象的类型,咱们不能直接定义一个变量类型为泛型。
简单来讲,区别于平时咱们对「值」进行编程,泛型是对「类型」进行编程。这个听起来比较抽象。以后咱们会经过若干实例带你理解这句话,你先留一个印象就好。
为了明白上面这句话,·首先要区分“值”和“类型”。
咱们平时写代码基本都是「对值编程」。好比:
if (person.isVIP) {
console.log('VIP') } if (cnt > 5) { // do something } const personNames = persons.map(p => p.name) ... 复制代码
能够看出这都是对具体的值进行编程,「这符合咱们对现实世界的抽象」。从集合论的角度上来讲, 值的集合就是类型,在 TS 中最简单的用法是对值限定类型,从根本上来讲是限定值的集合。这个集合能够是一个具体的集合,也能够是多个集合经过集合运算(交叉并)生成的新集合。
(值和类型)
再来看一个更具体的例子:
function t(name: string) {
return `hello, ${name}`; } t("lucifer"); 复制代码
字符串 "lucifer" 是 string 「类型」的一个具体「值」。 在这里 "lucifer" 就是值,而 string 就是类型。
TS 明白 "lucifer" 是 string 集合中的一个元素,所以上面代码不会有问题,可是若是是这样就会报错:
t(123);
复制代码
由于 123 并非 string 集合中的一个元素。
对于 t("lucifer")而言,TS 判断逻辑的伪代码:
v = getValue(); // will return 'lucifer' by ast
if (typeof v === "string") { // ok } else { throw "type error"; } 复制代码
❝因为是静态类型分析工具,所以 TS 并不会执行 JS 代码,但并非说 TS 内部没有执行逻辑。
❞
简单来总结一下就是: 值的集合就是类型,平时写代码基本都是对值编程,TS 提供了不少「类型」(也能够自定义)以及不少「类型操做」帮助咱们「限定值以及对值的操做」。
上面已经铺垫了一番,你们已经知道了值和类型的区别,以及 TS 究竟帮咱们作了什么事情。可是直接理解泛型仍然会比较吃力,接下来我会经过若干实例,慢慢带你们走进泛型。
首先来思考一个问题:为何要有泛型呢
?这个缘由实际上有不少,在这里我选择你们广泛认同的一个切入点来解释。若是你明白了这个点,其余点相对而言理解起来会比较轻松。仍是经过一个例子来进行说明。
假如让你实现一个函数 id
,函数的参数能够是任何值,返回值就是将参数原样返回,而且其只能接受一个参数,你会怎么作?
你会以为这很简单,顺手就写出这样的代码:
const id = (arg) => arg;
复制代码
❝有的人可能以为 id 函数没有什么实际做用。其实否则, id 函数在函数式编程中应用很是普遍。
❞
因为其能够接受任意值,也就是说你的函数的入参和返回值都应该能够是任意类型。 如今让咱们给代码增长类型声明:
type idBoolean = (arg: boolean) => boolean;
type idNumber = (arg: number) => number; type idString = (arg: string) => string; ... 复制代码
一个笨的方法就像上面那样,也就是说 JS 提供多少种类型,就须要复制多少份代码,而后改下类型签名。这对程序员来讲是致命的。这种复制粘贴增长了出错的几率,使得代码难以维护,牵一发而动全身。而且未来 JS 新增新的类型,你仍然须要修改代码,也就是说你的代码「对修改开放」,这样很差。还有一种方式是使用 any 这种“万能语法”。缺点是什么呢?我举个例子:
id("string").length; // ok
id("string").toFixed(2); // ok id(null).toString(); // ok ... 复制代码
若是你使用 any 的话,怎么写都是 ok 的, 这就丧失了类型检查的效果。实际上我知道我传给你的是 string,返回来的也必定是 string,而 string 上没有 toFixed 方法,所以须要报错才是我想要的。也就是说我真正想要的效果是:当我用到id的时候,你根据我传给你的类型进行推导
。好比我传入的是 string,可是使用了 number 上的方法,你就应该报错。
为了解决上面的这些问题,咱们「使用泛型对上面的代码进行重构」。和咱们的定义不一样,这里用了一个 类型 T,这个 「T 是一个抽象类型,只有在调用的时候才肯定它的值」,这就不用咱们复制粘贴无数份代码了。
function id<T>(arg: T): T {
return arg; } 复制代码
为何这样就能够了? 为何要用这种写法?这个尖括号什么鬼?万物必有因果,之因此这么设计泛型也是有缘由的。那么就让我来给你们解释一下,相信不少人都没有从这个角度思考过这个问题。
上面提到了一个重要的点 平时咱们都是对值进行编程,泛型是对类型进行编程
。上面我没有给你们解释这句话。如今铺垫足够了,那就让咱们开始吧!
继续举一个例子:假如咱们定义了一个 Person 类,这个 Person 类有三个属性,而且都是必填的。这个 Person 类会被用于用户提交表单的时候限定表单数据。
enum Sex {
Man, Woman, UnKnow, } interface Person { name: string; sex: Sex; age: number; } 复制代码
忽然有一天,公司运营想搞一个促销活动,也须要用到 Person 这个 shape
,可是这三个属性均可以选填,同时要求用户必须填写手机号以便标记用户和接受短信。一个很笨的方法是从新写一个新的类:
interface MarketPerson {
name?: string; sex?: Sex; age?: number; phone: string; } 复制代码
❝还记得我开头讲的重复类型定义么? 这就是!
❞
这明显不够优雅。若是 Person 字段不少呢?这种重复代码会异常多,不利于维护。 TS 的设计者固然不容许这么丑陋的设计存在。那么是否能够根据已有类型,生成新的类型呢?固然能够!答案就是前面我提到了两种对类型的操做:「一种是集合操做,另外一种是今天要讲的泛型。」
先来看下集合操做:
type MarketPerson = Person & { phone: string };
复制代码
这个时候咱们虽然添加了一个必填字段 phone,可是没有作到name, sex, age
选填,彷佛集合操做作不到这一点呀。咱们脑洞一下,假如咱们能够「像操做函数那样操做类型」,是否是有可能呢?好比我定义了一个函数 Partial
,这个函数的功能入参是一个类型,返回值是新的类型,这个类型里的属性所有变成可选的。
伪代码:
function Partial(Type) { type ans = 空类型 for(k in Type) { 空类型[k] = makeOptional(Type, k) } return ans } type PartialedPerson = Partial(Person) 复制代码
惋惜的是上面代码不能运行,也不可能运行。不可能运行的缘由有:
所以迫切须要一种不依赖 JS 行为,特别是运行时行为的方式,而且逻辑其实和上面相似的,且不会和现有语法体系冲突的语法。 咱们看下 TS 团队是怎么作的:
// 能够当作是上面的函数定义,能够接受任意类型。因为是这里的 “Type” 形参,所以理论上你叫什么名字都是无所谓的,就好像函数定义的形参同样。
type Partial<Type> = { do something } // 能够当作是上面的函数调用,调用的时候传入了具体的类型 Person type PartialedPerson = Partial<Person> 复制代码
先无论功能,咱们来看下这两种写法有多像:
(定义)
(运行)
再来看下上面泛型的功能。上面代码的意思是对 T 进行处理,是返回一个 T 的子集,具体来讲就是将 T 的全部属性变成可选。这时 PartialedPerson
就等于 :
interface Person {
name?: string; sex?: Sex; age?: number; } 复制代码
❝功能和上面新建一个新的 interface 同样,可是更优雅。
❞
最后来看下泛型 Partial 的具体实现,能够看出其没有直接使用 JS 的语法,而是本身定义了一套语法,好比这里的 keyof
,至此彻底应证了我上面的观点。
type Partial<T> = { [P in keyof T]?: T[P] };
复制代码
❝刚才说了“因为是形参,所以起什么名字无所谓” 。所以这里就起了 T 而不是 Type,更短了。这也算是一种约定俗称的规范,你们通常习惯叫 T, U 等表示泛型的形参。
❞
咱们来看下完整的泛型和函数有多像!
(定义)
(使用)
从外表看只不过是 function
变成了 type
,()
变成了 <>
而已。
从语法规则上来看, 函数内部对标的是 ES 标准。而泛型对应的是 TS 实现的一套标准。
简单来讲,将类型当作值,而后对类型进行编程,这就是泛型的基本思想。泛型相似咱们平时使用的函数,只不过其是做用在类型上,思想上和咱们平时使用的函数并无什么太多不一样,泛型产生的具体类型也支持类型的操做。好比:
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
复制代码
有了上面的知识,咱们经过几个例子来巩固一下。
function id<T, U>(arg1: T, arg2: U): T {
return arg1; } 复制代码
上面定义了泛型 id,其入参分别是 T 和 U,和函数参数同样,使用逗号分隔。定义了形参就能够在函数体内使用形参了。如上咱们在函数的参数列表和返回值中使用了形参 T 和 U。
返回值也能够是复杂类型:
function ids<T, U>(arg1: T, arg2: U): [T, U] {
return [arg1, arg2]; } 复制代码
(泛型的形参)
和上面相似, 只不过返回值变成了数组而已。
须要注意的是,思想上咱们能够这样去理解。可是具体的实现过程会有一些细微差异,好比:
type P = [number, string, boolean];
type Q = Date; type R = [Q, ...P]; // A rest element type must be an array type. 复制代码
再好比:
type Lucifer = LeetCode;
type LeetCode<T = {}> = { name: T; }; const a: LeetCode<string>; //ok const a: Lucifer<string>; // Type 'Lucifer' is not generic. 复制代码
改为这样是 ok 的:
type Lucifer<T> = LeetCode<T>;
复制代码
为何泛型要用尖括号(<>),而不是别的? 我猜是由于它和 () 长得最像,且在如今的 JS 中不会有语法歧义。可是,它和 JSX 不兼容!好比:
function Form() { // ... 复制代码return ( <Select<string> options={targets} value={target} onChange={setTarget} /> ); } 复制代码
这是由于 TS 发明这个语法的时候,还没想过有 JSX 这种东西。后来 TS 团队在 TypeScript 2.9 版本修复了这个问题。也就是说如今你能够直接在 TS 中使用带有泛型参数的 JSX 啦(好比上面的代码)。
实际上除了上面讲到的函数泛型,还有接口泛型和类泛型。不过语法和含义基本同函数泛型同样:
interface id<T, U> {
id1: T; id2: U; } 复制代码
(接口泛型)
class MyComponent extends React.Component<Props, State> {
... } 复制代码
(类泛型)
总结下就是: 泛型的写法就是在标志符后面添加尖括号(<>),而后在尖括号里写形参,并在 body(函数体, 接口体或类体) 里用这些形参作一些逻辑处理。
正如文章开头那样,咱们能够对函数的参数进行限定。
function t(name: string) {
return `hello, ${name}`; } t("lucifer"); 复制代码
如上代码对函数的形参进行了类型限定,使得函数仅能够接受 string 类型的值。那么泛型如何达到相似的效果呢?
type MyType = (T: constrain) => { do something };
复制代码
仍是以 id 函数为例,咱们给 id 函数增长功能,使其不只能够返回参数,还会打印出参数。熟悉函数式编程的人可能知道了,这就是 trace 函数,用于调试程序。
function trace<T>(arg: T): T {
console.log(arg); return arg; } 复制代码
假如我想打印出参数的 size 属性呢?若是彻底不进行约束 TS 是会报错的:
❝注意:不一样 TS 版本可能提示信息不彻底一致,个人版本是 3.9.5。下文的全部测试结果均是使用该版本,再也不赘述。
❞
function trace<T>(arg: T): T {
console.log(arg.size); // Error: Property 'size doesn't exist on type 'T' return arg; } 复制代码
报错的缘由在于 T 理论上是能够是任何类型的,不一样于 any,你无论使用它的什么属性或者方法都会报错(除非这个属性和方法是全部集合共有的)。那么直观的想法是限定传给 trace 函数的「参数类型」应该有 size 类型,这样就不会报错了。如何去表达这个「类型约束」的点呢?实现这个需求的关键在于使用类型约束。 使用 extends 关键字能够作到这一点。简单来讲就是你定义一个类型,而后让 T 实现这个接口便可。
interface Sizeable {
size: number; } function trace<T extends Sizeable>(arg: T): T { console.log(arg.size); return arg; } 复制代码
这个时候 T 就再也不是任意类型,而是被实现接口的 shape,固然你也能够继承多个接口。「类型约束是很是常见的操做,你们必定要掌握。」
❝有的人可能说我直接将 Trace 的参数限定为 Sizeable 类型能够么?若是你这么作,会有类型丢失的风险,详情能够参考这篇文章A use case for TypeScript Generics[1]。
❞
你们平时写 TS 必定见过相似 Array<String>
这种写法吧? 这实际上是集合类,也是一种泛型。
本质上数组就是一系列值的集合,这些值能够能够是任意类型,数组只是一个容器而已。然而平时开发的时候一般数组的项目类型都是相同的,若是不加约束的话会有不少问题。 好比我应该是一个字符串数组,然是却不当心用到了 number 的方法,这个时候类型系统应该帮我识别出这种「类型问题」。
因为数组理论能够存听任意类型,所以须要使用者动态决定你想存储的数据类型,而且这些类型只有在被调用的时候才能去肯定。 Array<String>
就是调用,通过这个调用会产生一个具体集合,这个集合只能存放 string 类型的值。
不调用直接把 Array 是不被容许的:
const a: Array = ["1"];
复制代码
如上代码会被错:Generic type 'Array<T>' requires 1 type argument(s).ts
。 有没有以为和函数调用没传递参数报错很像?像就对了。
这个时候你再去看 Set, Promise,是否是很快就知道啥意思了?它们本质上都是包装类型,而且支持多种参数类型,所以能够用泛型来约束。
你们若是开发过 React 的 TS 应用,必定知道 React.FC
这个类型。咱们来看下它是如何定义[2]的:
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> { (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null; propTypes?: WeakValidationMap<P>; contextTypes?: ValidationMap<any>; defaultProps?: Partial<P>; displayName?: string; } 复制代码
能够看出其大量使用了泛型。你若是不懂泛型怎么看得懂呢?无论它多复杂,咱们从头一点点分析就行,记住我刚才讲的类比方法,将泛型类比到函数进行理解。·
❝所以,实际上第一行代码的做用就是起了一个别名
❞
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
的含义是 FunctionComponent 是一个函数,接受两个参数(props 和 context )返回 ReactElement 或者 null。ReactElement 你们应该比较熟悉了。
PropsWithChildren
实际上就是往 props 中插入 children,源码也很简单,代码以下:
type PropsWithChildren<P> = P & { children?: ReactNode };
复制代码
这不就是咱们上面讲的「集合操做」和 「可选属性」么?至此,React.FC 的全貌咱们已经清楚了。读者能够试着分析别的源码检测下本身的学习效果,好比 React.useState
类型的签名。
类型推导和默认参数是 TS 两个重要功能,其依然能够做用到泛型上,咱们来看下。
咱们通常常见的类型推导是这样的:
const a = "lucifer"; // 咱们没有给 a 声明类型, a 被推导为 string
a.toFixed(); // Property 'toFixed' does not exist on type 'string'. a.includes("1"); // ok 复制代码
须要注意的是,类型推导是仅仅在初始化的时候进行推导,以下是没法正确推导的:
let a = "lucifer"; // 咱们没有给 a 声明类型, a 被推导为string
a.toFixed(); // Property 'toFixed' does not exist on type 'string'. a.includes("1"); // ok a = 1; a.toFixed(); // 依然报错, a 不会被推导 为 number 复制代码
而泛型也支持类型推导,以上面的 id 函数为例:
function id<T>(arg: T): T {
return arg; } id<string>("lucifer"); // 这是ok的,也是最完整的写法 id("lucifer"); // 基于类型推导,咱们能够这样简写 复制代码
这也就是为何 useState 有以下两种写法的缘由。
const [name, setName] = useState("lucifer");
const [name, setName] = useState<string>("lucifer"); 复制代码
实际的类型推导要更加复杂和智能。相信随着时间的推动,TS 的类型推导会更加智能。
和类型推导
相同的点是,默认参数也能够减小代码量,让你少些代码。前提是你要懂,否则伴随你的永远是大大的问号。其实你彻底能够将其类比到函数的默认参数来理解。
举个例子:
type A<T = string> = Array<T>;
const aa: A = [1]; // type 'number' is not assignable to type 'string'. const bb: A = ["1"]; // ok const cc: A<number> = [1]; // ok 复制代码
上面的 A 类型默认是 string 类型的数组。你能够不指定,等价于 Array,固然你也能够显式指定数组类型。有一点须要注意:在 JS 中,函数也是值的一种,所以:
const fn = () => null; // ok
复制代码
可是泛型这样是不行的,这是和函数不同的地方(设计缺陷?Maybe):
type A = Array; // error: Generic type 'Array<T>' requires 1 type argument(s).
复制代码
其缘由在与 Array 的定义是:
interface Array<T> {
... } 复制代码
而若是 Array 的类型也支持默认参数的话,好比:
interface Array<T = string> {
... } 复制代码
那么 type A = Array;
就是成立的,若是不指定的话,会默认为 string 类型。
若是你认真看完本文,相信应该知道何时使用泛型了,我这里简单总结一下。
当你的函数,接口或者类:
上面说了泛型和普通的函数有着不少类似的地方。普通的函数能够嵌套其余函数,甚至嵌套本身从而造成递归。泛型也是同样!
好比:
type CutTail<Tuple extends any[]> = Reverse<CutHead<Reverse<Tuple>>>;
复制代码
如上代码中, Reverse 是将参数列表反转,CutHead 是将数组第一项切掉。所以 CutTail 的意思就是将传递进来的参数列表反转,切掉第一个参数,而后反转回来。换句话说就是切掉参数列表的最后一项。 好比,一个函数是 function fn (a: string, b: number, c: boolean):boolean {},那么通过操做type cutTailFn = CutTail<typeof fn>
,能够返回(a: string, b:number) => boolean
。 具体实现能够参考Typescript 复杂泛型实践:如何切掉函数参数表的最后一个参数?[3]。 在这里,你知道泛型支持嵌套就够了。
泛型甚至能够嵌套本身从而造成递归,好比咱们最熟悉的单链表的定义就是递归的。
type ListNode<T> = {
data: T; next: ListNode<T> | null; }; 复制代码
(单链表)
再好比 「HTMLElement」 的定义。
declare var HTMLElement: {
prototype: HTMLElement; new(): HTMLElement; };。 复制代码
(HTMLElement[4])
上面是「递归声明」,咱们再来看一个更复杂一点的递归形式 - 「递归调用」,这个递归调用的功能是:「递归地将类型中全部的属性都变成可选」。相似于深拷贝那样,只不过这不是拷贝操做,而是变成可选,而且是做用在类型,而不是值。
type DeepPartial<T> = T extends Function
? T : T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T; type PartialedWindow = DeepPartial<Window>; // 如今window 上全部属性都变成了可选啦 复制代码
虽然泛型支持函数的嵌套,甚至递归,可是其语法能力确定和 JS 无法比, 想要实现一个泛型功能真的不是一件容易的事情。这里提供几个例子,看完这几个例子,相信你至少能够达到比葫芦画瓢的水平。这样多看多练,慢慢水平就上来了。
截止目前(2020-06-21),TS 提供了 16 种工具类型[5]。
(官方提供的工具类型)
除了官方的工具类型,还有一些社区的工具类型,好比type-fest[6],你能够直接用或者去看看源码看看高手是怎么玩类型的。
我挑选几个工具类,给你们讲一下「实现原理」。
功能是将类型的属性「变成可选」。注意这是浅 Partial,DeepPartial 上面我讲过了,只要配合递归调用使用便可。
type Partial<T> = { [P in keyof T]?: T[P] };
复制代码
功能和Partial
相反,是将类型的属性「变成必填」, 这里的 -
指的是去除。 -?
意思就是去除可选,也就是必填啦。
type Required<T> = { [P in keyof T]-?: T[P] };
复制代码
功能是将类型的属性「变成可修改」,这里的 -
指的是去除。 -readonly
意思就是去除只读,也就是可修改啦。
type Mutable<T> = {
-readonly [P in keyof T]: T[P]; }; 复制代码
功能和Mutable
相反,功能是将类型的属性「变成只读」, 在属性前面增长 readonly
意思会将其变成只读。
type Readonly<T> = { readonly [P in keyof T]: T[P] };
复制代码
功能是用来获得一个函数的返回值类型。
type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[] ) => infer R ? R : any; 复制代码
下面的示例用 ReturnType 获取到 Func 的返回值类型为 string,因此,foo 也就只能被赋值为字符串了。
type Func = (value: number) => string;
const foo: ReturnType<Func> = "1"; 复制代码
更多参考TS - es5.d.ts[7] 这些泛型能够极大减小你们的冗余代码,你们能够在本身的项目中自定义一些工具类泛型。
最后介绍一个实用的小技巧。以下是一个接口的类型定义:
interface Seal {
name: string; url: string; } interface API { "/user": { name: string; age: number; phone: string }; "/seals": { seal: Seal[] }; } const api = <URL extends keyof API>(url: URL): Promise<API[URL]> => { return fetch(url).then((res) => res.json()); }; 复制代码
咱们经过泛型以及泛型约束,实现了智能提示的功能。使用效果:
(接口名智能提示)
(接口返回智能提示)
原理很简单,当你仅输入 api 的时候,其会将 API interface 下的全部 key 提示给你,当你输入某一个 key 的时候,其会根据 key 命中 interface 定义的类型,而后给予类型提示。
学习 Typescript 并非一件简单的事情,尤为是没有其余语言背景的状况。而 TS 中最为困难的内容之一恐怕就是泛型了。
泛型和咱们平时使用的函数是很像的,若是将二者进行横向对比,会很容易理解,不少函数的都关系能够迁移到泛型,好比函数嵌套,递归,默认参数等等。泛型是对类型进行编程,参数是类型,返回值是一个新的类型。咱们甚至能够对泛型的参数进行约束,就相似于函数的类型约束。
最后经过几个高级的泛型用法以及若干使用的泛型工具类帮助你们理解和消化上面的知识。要知道真正的 TS 高手都是玩类型的,高手才不会知足于类型的交叉并操做。 泛型用的好确实能够极大减小代码量,提升代码维护性。若是用的太深刻,也可能会团队成员面面相觑,一脸茫然。所以抽象层次必定要合理,不只仅是泛型,整个软件工程都是如此。
你们也能够关注个人公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。
A use case for TypeScript Generics: https://juliangaramendy.dev/when-ts-generics/
[2]React.FC Type Definition: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts
[3]Typescript 复杂泛型实践:如何切掉函数参数表的最后一个参数?: https://zhuanlan.zhihu.com/p/147248333
[4]HTMLElement Type Definition: https://github.com/microsoft/TypeScript/blob/master/lib/lib.dom.d.ts
[5]TS 官方的16 种工具类型: https://www.typescriptlang.org/docs/handbook/utility-types.html#partialt
[6]type-fest: https://github.com/sindresorhus/type-fest
[7]TS - es5.d.ts: https://github.com/microsoft/TypeScript/blob/master/src/lib/es5.d.ts#L1431