介绍

官网原话:软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能;

好吧,有点云里雾里,不大理解,简单的说就是TS中的类型强制性太高,比如

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

如果我们这个函数要被复用,基本很难,因为类型完全限定死了,那如果把类型放开呢,比如any,那还写啥ts…js不香吗,受这罪…到这里,泛型的作用就体现了,它没有对类型限定死,但是又比any来的严格的多,下面我们具体看
一下;

泛型

我个人觉得泛型就是在定义的时候,定义了一个虚拟的类型,这个类型需要在被使用的时候才确认,使用的时候是string类型的,那么泛型就是string类型,使用的时候是number类型,那么泛型就是number类型;

而且,我一直认为TS其实就是在玩类型,花式玩类型,很多时候TS的类型注解和JS的代码混在一起,根本就看不懂到底写了个啥…所以任重而道远啊;

泛型简单的来说分为两种,一种叫做:函数泛型,另外一种就是类泛型

函数泛型

顾名思义,使用在函数中的泛型,先直接看个例子

function join<T>(first: T, second: T) {
    return `${first}-${second}`;
}

通过<>这个符号来定义一个泛型,<>里面的是代表这个泛型的类型,这个类型可以是任意定义的,并不一定是上例中的T,这个T只是一个代指,具体是什么类型需要等到使用的时候确定,比如下例也是可以的:

function join<ABD>(first: ABD, second: ABD) {
    return `${first}-${second}`;
}

等到使用的时候,只需要指定类型就可以了

// 正确
join<string>("1","2");
join<number>(1,2);

// 错误
join<string>(1,"2");
join<string>("1",2);

再看一个例子:

function join<ABD>(first: ABD[], second: Array<ABD>) {
    return [...first,...second];
}

我们定义了一个这样的函数,它也是一个函数泛型,但是与之前的例子不同的是,它的两个参数是有类型ABD组成的数组

join<string>("123")	// 报错
join<string>(["123"])	// 正确

当然,泛型可以写不止一个,再看一个例子,泛型可以写不止一个,多个也是可以的

function join<T,A>(first: T, second: A) {
    return `${first}-${second}`;
}

join<string,number>("1",123)	// 正确

// 甚至,TS是能根据定义的时候做类型推断的

join("1",123)	// 正确,这么写也没有问题,TS能自己推断出具体类型是什么

可能有的同学看到这里会说,这个泛型确实比一般的类型复杂了点,但是也没上面说的复杂到看不懂啊,好吧,到这里其实只是一些基础知识,来着稍微复杂一点的试炼一下

const getData: <T>(str: T) => string = <T>(str: T): string => {
    return `${str}-1`;
};

emmm…,第一反应肯定是有点懵,这是个啥,有点像箭头函数,但是为什么会有两个箭头函数在一起,好吧,我们具体解释一下:
首先我们确定这是一个函数表达式,然后后面应该是跟了一个箭头函数,然后返回的就是这个${str}-1,当中一部分应该是TS的类型定义,那我换个写法,看看能不能理解:

// 将箭头函数拆解出来
function handleData<T>(str: T): string {
    return `${str}-1`;
}

const getData: <T>(str: T) => string = handleData;

这样是不是好理解一点

// 函数名
const getData

// 函数的TS类型
 <T>(str: T) => string

// 函数体
function handleData<T>(str: T): string {
    return `${str}-1`;
}

// 对应到上面那个简写

// 函数名
const getData

// 函数的TS类型
 <T>(str: T) => string

// 函数体
<T>(str: T): string => {
    return `${str}-1`;
};

这样是不是能理解了,所以正如我上面所说,当TS的类型写法混入JS语法的时候,很容易让人就看不懂代码了,所以遇到这类代码必须静下心来好好分析,这类代码在一些框架中非常频繁的用到;


类泛型

泛类型就是在类中使用泛型,老规矩,还是直接看一个例子先:

class Join {
    constructor(private data: string[]) {}
    getData(index: number): string {
        return this.data[index];
    }
}

const data = new Join(["123", "456"]);
data.getData(0);

这个类代码初始化的时候接收一个由字符串组成的数组作为参数,代码本身没有问题,它的问题在于不够灵活,假如我们存储的数组中的类型不再单单是字符串类型,有可能是任何类型,并且返回的,那么,就必须写很长很长的联合类型,比如:

class Join {
    constructor(private data: string[]|number[]|boolean[]) {}
    getData(index: number): string|number|boolean {
        return this.data[index];
    }
}

const data = new Join(["123", "456"]);
data.getData(0);

很明显,这是不合理的,这种就可以用泛型改造,泛型的灵活性确实是TS中的一个亮点,那么泛型这么写,直接看例子吧,先看一个基础用法:

class Join<T> {
    constructor(private data: T[]) {}
    getData(index: number): T {
        return this.data[index];
    }
}

const data = new Join<string>(["123", "456"]);
data.getData(0);

和函数泛型意义,定义了一个T,并且存储的是由类型T组成的数组,通过getData返回的也是类型T,没有问题,那么如果是多个类型,也是同样的

class Join<T, P> {
    constructor(private data: (T | P)[]) {}
    getData(index: number): T | P {
        return this.data[index];
    }
}

const data = new Join<string, number>(["123", "456", 1, 2]);
data.getData(0);

到这里,我们基本说的都是基础类型,那么如果是对象这么弄,比如我们传入的是一个对象,并且确认每个对象上都有一个name属性

class Join<T> {
    constructor(private data: T[]) {}
    getData(index: number): T{
        return this.data[index].name;	// 报错,因为不能确定T上面存在name属性
    }
}

那么此时可以这么改写:

interface User {
    name: string;
}
class Join<T extends User> {
    constructor(private data: T[]) {}
    getData(index: number): T {
        return this.data[index];
    }
}

定义了一个接口(interface),然后让这个T去继承这个接口,那么我们就确认了传进来的每一个T上面都会存在一个name属性,如果使用的时候参数不对,那么Ts是会报错的

const data = new Join<string>(["123", "456"]);	// 报错,因为string上不存在name属性

const data = new Join<User>([{ name: "oliver" }]);	// 正确,类型换成了接口定义的类型


还有,还可以结束extends来对泛型进行约束,比如

interface User {
    name: string;
}
class Join<T extends string | number> {
    constructor(private data: T[]) {}
    getData(index: number): T {
        return this.data[index];
    }
}

const data = new Join<User>([{ name: "oliver" }]);	// 报错

const data = new Join<string>([{ name: "oliver" }]);	// 正确

类Join的泛型T,通过extends将其约束为string或者为number,因此等到具体使用的时候,就只能是string或者number了;

小结

泛型就是一种虚拟类型,它的具体类型必须要等到这个泛型被使用的时候才确定,这样的好处就是它的用法会非常的灵活,而不是一开始就限定死了其变量类型,它能给予开发者非常高的自由度,加油,用好泛型,就是高手,一起努力吧!