泛型?

我们之前已经接触到了TS里的各种类型定义,今天来看一种更灵活的类型-「泛型」。从字面意思来看,给一个定义的话,那就是「宽泛的类型」。比如我们之前定义一个数组项全是字符串的数组是这样来写的:

let arr:string[] = ['A','B','C'];

现在改成泛型:

let arr: Array<string> = ['A', 'B', 'C'];

上面的Array就称之为泛型,首先,这个单词告诉我们要定义一个数组类型,随后出现的尖括号< >则可以写任意的类型来限定里面的数组元素。这就是泛型的「宽泛」之处。

自定义泛型函数

我们可以使用泛型来描述一个函数,这里举个例子,假如我们想实现一个merge函数,在没用泛型的时候应该会这样写:


function merge(obj1: object, obj2: object) { return { ...obj1, ...obj2 }; } const mergedObj = merge({ name: 'Daniel' }, { age: 27 });

但是这样有一个问题,如果我们现在想把合并后的对象的name打印出来,发现:

这是为啥???其实这很简单,我们merge函数的返回结果并没有明确指定类型,所以我们直接通过mergedObj来找下面的属性是不行的。我们可以通过泛型来解决这个问题:

function merge<T, U>(obj1: T, obj2: U) {
    return { ...obj1, ...obj2 };
}

const mergedObj = merge({ name: 'Daniel' }, { age: 27 });

console.log(mergedObj.name);

嗯?TU是什么东西?其实它们就是我们定义泛型的参数,到这里我们就可以知道:泛型其实就是通过将类型「参数化」来实现的,这里我们将obj1设置成了T类型,将obj2设置成了U类型,问题来了,这两个东西是啥?以前没见过啊。。。其实这两个就是两个变量,我们可以通过传参来改变它们的值,可是上面的代码哪有传参过去?
是这样的,如果不传泛型参数的话,那么这个参数默认会根据你传的实参进行自动添加,例如上面的例子,其实它默认是传了泛型的参数的,如果手动写出来的话,应该是这样的:

function merge<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

const mergedObj = merge<{ name: string }, { age: number }>({ name: 'Daniel' }, { age: 27 });

console.log(mergedObj.name);

限制泛型类型

再看这段:

function merge<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

const mergedObj = merge({ name: 'Daniel' }, 27);    //这里直接传了一个数字!!!

console.log(mergedObj);

上面代码中,我们在调用merge的时候并不是传入两个对象,第二个参数传了一个数字27,但是仍然能编译通过。。。
因为我们并没有限制TU是是哪些类型,所以传啥都可以。所以我们现在要对其进行限制:

function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

我们直接让TU继承一个object,这样我们在传参的时候就不会传错了,传错就会直接报错:

keyof

下面再来看一个例子,我想写一个用来获取对象属性值的函数,传统思路应该是这样来写:

function getObjectValueByKey(obj: object, key: string) {
    return obj[key];
}

但是这样在TS里是不能工作的:

报错说string不能作为对象的的索引。这是因为在TS中,一个对象的key就是key,不能理解成是一个字符串,所以我们需要使用泛型再加上keyof关键字:

function getObjectValueByKey<T extends object, U extends keyof T>(obj: T, key: U) {
    return obj[key];
}

可以看到objT类型继承了objectkeyU类型又被T限制了,并且这个限制是很严格的,它必须是T所描述值中的key才可以,所以才叫keyof

泛型类

我们来新建一个存储数据的类:

class DataStorage {
    private data = [];

    addItem(item) {
        this.data.push(item);
    }
    remove(item) {
        this.data.splice(this.data.indexOf(item), 1);
    }
    getItems() {
        return this.data;
    }
}

这样很明显是一个不合格的类,因为类的属性和方法参数都没设置类型,所以报错是必须的:

那么我们现在可以通过添加泛型来解决这个问题:

class DataStorage<T> {
    private data: T[] = [];

    addItem(item: T) {
        this.data.push(item);
    }
    remove(item: T) {
        this.data.splice(this.data.indexOf(item), 1);
    }
    getItems() {
        return this.data;
    }
}

const stringStorage = new DataStorage<string>();

stringStorage.addItem('A');
stringStorage.addItem('B');
stringStorage.addItem('C');
stringStorage.removeItem('C');

console.log(stringStorage.getItems());

这里我们把T设置成了string类型,我们可以根据自己的需要传入其它类型。

高级内置泛型(部分)

Partial

如果我们根据这样一个接口来新建一个对象的话:

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

那么必须把所有的属性都写上才可以:

const person: Person = {
    name: 'Daniel',
    gender: '男',
    age: 27
};

如果我不想在新建对象的时候全部都写上这些属性,而是在一些逻辑操作之后在往上面添加要怎么办呢?之前在写JS的时候就直接这样写了:

const person = {
    name: 'Daniel'
};
person.gender = '男';
person.age = 27;

但是!这样写,在TS里怎么可能会不报错呢?。。。那么如何解决?
有两种方法,一种就是在接口定义的时候就直接把属性定义成可选的,也就是在属性后面加上一个问号-?

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

还有一种方法就是使用Partial泛型:

const person: Partial<Person> = {
    name: 'Daniel'
};
person.gender = '男';
person.age = 27;

我们使用PartialPerson里面的属性全都变成了可选。

Readonly

如果我们想创建一个只读的数组,你可能会想到使用const来创建一个常量:

const arr = ['A', 'B', 'C'];

但是你会发现,这样写并没用,为啥?因为我们虽然不能直接修改arr,但是我们仍然可以使用一些数组方法来改变这个数组:

const arr = ['A', 'B', 'C'];
arr.push('D');

有了Readonly泛型,我们可以很方便的实现这个功能:

const arr: Readonly<string[]> = ['A', 'B', 'C'];

这个时候如果再进行push就会报错: