TS 泛型

介绍

泛型:generics,参数化类型,全称为 **泛型参数**,我们接下来都简称为泛型 。

学过面向对象语言的小伙伴都知道继承。但是在这里我要说的是:继承不是某一门语言的特性,是某一类语言的特性。哪一类呢?答案是面向对象语言。好了,问题又来了,面向对象语言为什么要实现继承的特征呢?因为继承背后的思想是代码重用/复用/共享,编写的代码可以被许多派生类型的对象所重用。

因为我们要复用代码,所以有了继承。

有时候我们嫌代码的力度太小,想要复用文件,怎么办呢?

import 关键字应运而生。

在Java里面,import 运行后被编译器替换为包的路径限定名。语法 import 包名 帮助我们实现了文件的复用。

在JavaScript里面, import 文件名 运行后帮助我们在作用域链上方建立了一个 module 作用域。

import 帮助我们实现了文件的复用。

但是在下面这种场景下如果我们想要实现算法的复用,怎么办呢?

注意:

  • C 语言中代码 = 算法 + 数据结构 。
  • Java语言中 代码 = (数据结构 + 算法)

场景1:

写一个函数,这个函数会返回任何传入的值。

eg:不用泛型的话,这个函数可能是下面这样:

function identity(arg: number): number {
    return arg;
}

如果我们要编写框架,就要考虑到各种返回值的情况,于是可能就会有这样的代码:

type idBoolean = (arg: boolean) => boolean;
type idNumber = (arg: number) => number;
type idString = (arg: string) => string;

很明显,我们的这些代码逻辑是相同的,但是由于需要,我们不得不写 n 遍这样的逻辑。

有些小伙伴可能会说,我们可以使用any类型来定义函数:

function identity(arg: any): any {
    return arg;
}

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。

所以为了解决算法的复用,我们引入了泛型的概念。如下所示:

function identity<T>(arg: T): T {
    return arg;
}

我们给identity添加了类型变量T。 T帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。

接下来我们就可以通过这样的语法调用 identity 函数。

identity<string>('hello');

泛型就是创建类型变量用以接受任意类型。

小结

import 解决了文件的复用。

继承解决了代码的复用。

泛型解决了算法的复用。

泛型是什么?

普通函数要求我们对 编程,泛型则要求我们对 类型 编程。

啊?天呐!类型还可以编程?当初笔者也比较迷惑,类型怎样编程,后来随着对TS的深入理解,逐渐清晰了所谓的类型编程值编程

类型编程

接下来带你体验一下所谓的类型编程。

我们在 TypeScript 中有这样两个操作符 ‘|’ 和 ‘&’。

  • ‘|’:取并集运算符。
  • ‘&’:取交集运算符。

我们先来看看集合的概念。

文氏图

文氏图用于展示在不同的事物群组(集合)之间的数学或逻辑联系,尤其适合用来表示集合(或)类之间的 “大致关系”,它也常常被用来帮助推导(或理解推导过程)关于集合运算(或类运算)的一些规律。

在文氏图法中,如果有论域,则以一个矩形框(的内部区域)表示论域;各个集合(或类)就以圆/椭圆(的内部区域)来表示。两个圆/椭圆相交,其相交部分表示两个集合(或类)的公共元素,两个圆/椭圆不相交(相离或相切,而实际上在文氏图中相切是没有什么意义的,因为文氏图是以图形的内部区域来表示的)则说明这两个集合(或类)没有公共元素。

比如黄色的圆圈(集合 A)可以表示两足的所有动物。蓝色的圆圈(集合 B)可以表示会飞的所有动物。黄色和蓝色的圆圈交叠的区域(叫做交集)包含会飞且两足的所有动物 —— 比如鹦鹉。 (把每个单独的动物类型想像为在这个图中的某个点)。

image.png
(图片来源:https://zh.wikipedia.org/wiki/文氏图)

在TypeScript中,我们的类型就是取自集合的思想。例如: string 类型是所有字符串的集合, number 是所有数字的集合。在 TS 中不仅有这些已经定义好的类型,还有可以提供我们自定义类型的关键字。例如: interface 。我们使用联合类型(A|B)表示集合中的元素属于 A 或者属于 B 。使用交叉类型(A&B)表示集合中的元素既属于 A 又属于 B

如果 A = {name: string}B = {age: number},那么
属于 A 或者属于 B的元素(A|B)为仅仅包含name或仅仅包含age或包含name和age的对象,既属于 A 又属于 B的元素(A&B)为包含name和age的对象。

示例1:

在 TypeScript 中编程要注意将文件的

interface cat = {name: string, purrs: boolean};
interface dog = {name: string, barks: boolean};
interface catAndDog = cat & dog;
interface catOrDog = cat | dog;
let a: catOrDog = {name: 'jack',purrs: true,barks: boolean};
a = {name: 'jack', purrs: true};
a = {name: 'jack', barks: true};
let b: catAndDog = {name: 'tom',purrs: true,barks: boolean}

示例2:

假设我们定义了一个person shape:

interface Person {
    name: string,
    age: number,
    phone: string
}

有一个需求是填写表单并统计,其中 name agephone 是k可选的,但是 age 是可选的。我们可以再造一个接口

interface Person {
    name?: string,
    age?: number,
    phone?: string
}

好了,我们的需求解决了,啊哈。

不过TypeScript为我们提供了一种更好的方式----Partial。我们还可以通过这种方式 Partial<Person> 实现上面的需求。

嗯????Partial帮助我们完成了什么工作呢?怎么就这么突然就帮我们将 Person 的必选项转化为可选项了呢?我们来猜想一下他的实现步骤~

// js伪代码
functon Partial(Type){
    // 遍历Type
    for(k in Type){
        // 将Type中的每个元素转化为可选的
        Type[k] = Optional(k);
    }
    return Type;
}

// 可以看成是上面的函数定义,可以接受任意类型。由于是这里的 “Type” 形参,因此理论上你叫什么名字都是无所谓的,就好像函数定义的形参一样。
type Partial<Type> = { do something }
// 可以看成是上面的函数调用,调用的时候传入了具体的类型 Person
type PartialedPerson = Partial<Person>

接下来看下函数调用和类型调用写法对比。

image.png

image.png

真实的 Partial 是什么样的呢?

type Partial<T> = {
    [P in keyof T]?: T[P];
};

我们之前说类型编程,这里 Partial 的实现就是类型编程的样例。记住这里是类型的运算,所以 keyof in ? : 是集合操作符。

  • keyof :获取集合所有键的类型,合并为一个字符串字面量类型。假如 T 的类型为{a:number,b:string},,则 keyof T"a" | "b"
  • in :遍历集合中的元素。[P in keyof T] 遍历 keyof T 的结果集。
  • ?: :选择性映射。[P in keyof T]?: T[P] 将遍历的结果映射到 T[P] 上。

为了方便我们编程,TypeScript 中的对类型操作的语法类似面向对象语言中对值的语法。我们来看看对泛型的操作是不是和函数操作很像:

image.png

image.png

  • 从外表看只不过是 function 变成了 type() 变成了 <>而已。
  • 从语法规则上来看, 函数内部对标的是 ES 标准。而泛型对应的是 TS 实现的一套标准

image.png

泛型的用法就是对类型进行编程。通过 TypeScript 提供的集合操作符对泛型进行编程能够拼接得到任意想要的类型。

但是注意,我们在编写 .ts 文件的时候,使用的是 TypeScript 语法。部分内容遵守ECMAScript规范,但是牵涉到对类型编程的时候ECMAScript规范没有定义,所以是TypeScript自己定义的,所以同一个操作符在不同位置可能有不同的含义。例如 中括号[] ,有时候代表数组取值,有时候代表类型键入。其中这样的例子还有很多。

什么时候绑定泛型

声明泛型的位置不仅会限定泛型的作用域,还决定了什么时候为泛型绑定具体的类型。

T 在调用签名中声明(位于签名开始的圆括号前),TypeScript 将在调用 Filter 类型的函数时为T绑定具体的类型。

type Filter = {
    <T>(array: T[], f: (item: T) => boolean): T[]
}

let filter: Filter = (array, f) => ......

如果想要把 T 的作用域限定在类型别名 Filter 中,则要求在使用 Filter 的时候显示绑定类型。

type Filter<T> = {
    (array: T[], f: (item: T) => boolean): T[]
}

let filter: Filter = (array, f) => ...... // error
let filter: Filter<number> = (array, f) => ......

通常情况下:

  • 函数在被调用时绑定类型
  • 接口在实现时绑定类型
  • 类在实例化时绑定类型

可以在什么地方使用泛型

可以在任何支持调用值的地方实现泛型。

// 作用域覆盖整个签名
1type Filter<T> = {
    (array: T[], f: (item: T) => boolean): T[]
}

// 作用域覆盖函数调用期
2type Filter = {
    <T>(array: T[], f: (item: T) => boolean): T[]
}

// 3 是 2 的简写形式
3type Filter = <T>(array: T[], f: (item: T) => boolean): T[]

// 4 是 1 的简写形式
4type Filter<T> = (array: T[], f: (item: T) => boolean): T[]

// 具名函数调用签名
5function Filter<T>(array: T[], f: (item: T) => boolean): T[] { ...... }

泛型别名和泛型推导

泛型推导

在调用函数的过程中,TS能够根据我们传入的值推导出泛型的类型。

// 将T[]的数组映射为U[]类型的数组
function map<T, U>(array: T[], f: (item: T) => U): U[]{
    let result = [];
    for(let i = 0; i < array.length; i ++){
        result[i] = f(array[i])
    }
    return result;
}
// 调用map函数,经过TS的推导,T的类型是string,U的类型是boolean。
map(['a', 'b', 'c'], _ => _ === 'a');

泛型别名

泛型别名很容易理解。就是在我们的自定义类型中使用泛型,上文中的 Partial<T> 就是泛型别名。我们还可以根据自己的需求定义属于自己的泛型别名。

例如我们定义一个MyEvent类型,描述DOM事件,例如click或mousedown:

type MyEvent<T> = {
    target: T;
    type: string;
}

使用MyEvent这样的泛型别名时,一定要绑定类型参数,TS无法自动推导。

let event: MyEvent<HTMLButtonElement | null> = {
    target: document.querySelector('#myButton'),
    type: 'click'
}

使用类型别名构建其他类型。

type TimeEvent<T> = {
    MyEvent<T>;
    from: Date;
    to: Date;
}

在函数签名中使用泛型别名。

function triggerEvent<T>(event: MyEvent<T>): void {
// ...
}

// 调用triggerEvent时传入了一个参数,这个参数默认是MyEvent类型。
// TS发现传给对象target的值为document.querySelector('#myButton'),这意味着 T 为 document.querySelector('#myButton') 类型。
triggerEvent({
    target: document.querySelector('#myButton'),
    type: 'mouseOver'
})

Promise注意事项

TS 官方定义的 Promise 为:

interface Promise<T> {
    then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
    catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}

Promise<T> 是比较经典的泛型别名的使用。

泛型约束

正如文章开头那样,我们可以对函数的参数进行限定。

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 是会报错的:

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;
}

注意这里的extends不是继承,准确来讲是限制,限制 T 的类型为 Sizeable 的子集。Sizeable 中的属性一定要出现在 T 中出现。

泛型默认类型

泛型在定义的时候可以指定默认类型。

以 MyEvent 来举例

type MyEvent<T> = {
    target: T;
    type: string;
}

为了给事先不知道 MyEvent 将会绑定何种元素的情况提供便利,我们可以给 T 指定一个默认的类型。

type MyEvent<T = HTMLElement> = {
    target: T;
    type: string;
}

参考文章

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值