- Published on
Generics
- Authors
- Name
- Jack Fan
Generics
Built-in Genrics & What are Generics
当我们定义一个变量不确定类型的时候有两种解决方式:any 或 泛型。 先来看看内建的默认的泛型,Array。
const names = ["Jack"]; // type string[]
// ---------------------------------------------------------
const names = []; // type any[]
我们上下两种都是建立数组的方法,他们现在的类型会变得不同。如果我们给他指明类型呢?指明它就是 Array
const names: Array = [];
这会报错,提示Generic type 'Array<T>' requires 1 type arguments(s)
,即泛型类型Array<T>
需要 1 个类型参数。 当出现<>
的时候,说明我们正在处理一个泛型。
泛型指的是在定义函数/接口/类型时,不预先指定具体的类型,而是在使用的时候在指定类型限制的一种特性。
在这里,数组本身就是一种类型。但是,数组本身其实并不关心什么东西存在里面。但我们任然要说明,会存什么类型的东西进去。
const names: Array<string> = []; // string[]
names[0].split(" ");
这个时候,其实和直接指定类型为 string[]
是一样的。这个时候,我们可以在它的 item
上调用 split
了,因为我们知道他肯定是 string。
另一个内建泛型就是 Promise。
const promise = new Promise((res, rej) => {
setTimeout(() => {
res("Resolved!");
}, 2000);
});
这个时候去看 promise 的类型,会发现为const promise: Promise<unknown>
,那么这有什么用呢? 现在我们限定它返回的为 string。
const promise: Promise<string> = new Promise((res, rej) => {
setTimeout(() => {
res("Resolved!");
}, 2000);
});
promise.then((res) => res.split(" "));
我们在接下来的 then
调用的时候,就可以获得更好的对数据处理的支持。TypeScript 和编辑器知道我们返回的是 string 类型,所以我们可以使用 split 方法。
Creating a Generic Function
先来看看正常情况会有什么问题。
function merge(objA: object, objB: object) {
return Object.assign(objA, objB);
}
const mergeObj = merge({ name: "Jack" }, { age: 30 });
console.log(mergeObj.name);
在这里,mergeObj.name
会报错,因为我们虽然合并了两个对象,但是 TypeScript 还是不知道,mergeObj
会有 name 属性,除非加上一句:
const mergeObj = merge({ name: "Jack" }, { age: 30 }) as {
name: string;
age: number;
};
所以我们需要泛型。
function merge<T, U>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
在这里,我们在函数名字后面加上一个<>
,第一个写个 T(不一定是 T,但通常是),第二个写个 U,因为我们有这两个参数。随后在两个参数后面分别指定类型为 T 和 U。
此处如果报错,可以写 <T extends object, U extends object>
是因为 assign 方法需要确定你是一个 object,否则可以不写。
此时我们来看看函数的类型定义。
function merge<T extends object, U extends object>(objA: T, objB: U): T & U
可以看到,TypeScript 推断出这个函数返回的将是两个 object 的结合。我们就可以正常访问属性了。
mergeObj 的 type 此时为 const mergeObj: {name: string;} & { age: number; }
当然我们也可以明确指明参数类型,但这是多此一举。
const mergeObj = merge<{ name: string }, { age: number }>(
{ name: "Jack" },
{ age: 30 }
);
Working with Constrains
对于泛型中的类型,我们也可以做约束。 例如,对于刚才,如果我们这样定义 mergeObj
,age 就没有办法储存。
const mergeObj = merge({ name: "Jack" }, 30); // {name: 'Jack'}
所以我们可以做类型约束,我们希望我们的两个参数,一定为 object。
function merge<T extends object, U extends object>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
此时我们就不能传一个数字作为参数了,而必须是一个对象才可以。
Another Generics Function
再做一个泛型函数,这次我们只希望,参数他一定要包含某种属性即可。
interface Lengthy {
length: number;
}
function countAndDescribe<T extends Lengthy>(element: T): [T, string] {
let descriptionText =
element.length > 0 ? `Got ${element.length} elements.` : "Got no value.";
return [element, descriptionText];
}
console.log(countAndDescribe(["Sports"]));
console.log(countAndDescribe("I am a string!"));
这里我们定义了一个 interface,然后使用 extends 希望参数一定拥有这个 interface 里声明的属性(此处为 length)。 可以看到,在使用的时候,可以传入 string,也可以穿一个 Array,因为他们都具有 length 属性。
The keyof Constraint
function extractAndConvert(obj: object, key: string) {
return obj[key];
}
extractAndConvert({}, "name");
这个函数会有潜在的问题,如果我们传入的 key 写错了或者其他原因,并不是 obj 内部拥有的 key 怎么办?
function extractAndConvert<T extends object, U extends keyof T>(
obj: T,
key: U
) {
return obj[key];
}
extractAndConvert({ name: "Jack" }, "name");
这里使用了 keyof 关键字,它告诉 TypeScript,这个参数类型一定会是 T 这个 obj 的 key。所以,我们在使用的时候,第一个参数的 object 如果没有 name 这个 key,会报错。
Generics Classes
Class 也同样具有泛型
class DataStorage {
private data = [];
addItem(item) {
this.data.push(item);
}
removeItem(item) {
this.data.splice(this.data.indexOf(item), 1);
}
getItems() {
return [...this.data];
}
}
我们希望这个类里的 data 存的对象,只为 string 或只为 number 或其他等等,总之为单一类型,这个时候就可以使用泛型。
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data.splice(this.data.indexOf(item), 1);
}
getItems() {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
const numberStorage = new DataStorage<number>();
这里我们就做好了限制,如果想在 textStorage
加入 number 是不可能的,因为限定了其类型为 string,一切操作都和 string 相关,对于 numberStorage
也一样。 但对于 object 我们遇到了一些问题。
const objStorage = new DataStorage<object>();
objStorage.addItem({ name: "Jack" });
objStorage.addItem({ name: "Du" });
// ...
objStorage.removeItem({ name: "Jack" });
console.log(objStorage.getItems()); // { name: "Du" }
// Fail to remove, because the object is a totally new one, even if they have same content.
我们无法正确删除指定的 item,因为这是引用类型,对于 JavaScript 而言虽然内容相同,但确实一个全新的对象,indexOf
的返回值会是-1,也就是会把数组的最后一个元素给删掉。所以可以给 removeItem
做优化。
removeItem(item: T) {
if (this.data.indexOf(item) === -1) return;
this.data.splice(this.data.indexOf(item), 1);
}
但这并不是长久之计,也还有一种解决办法
const Lang = { name: "Lang" };
objStorage.addItem(Lang);
objStorage.removeItem(Lang);
将要添加的储存为一个固定变量,但这样会很复杂。所以最好的还是不要让这个类适用于 object,对于 object 应该有专门的 Class 最好。
class DataStorage<T extends string | number | boolean> {
//...
}
Generic Utility Types
TypeScript 自带了很多高级的类型,可以在编写代码的时候帮助我们。这些不会在编译的时候被编译,但却可以对我们的代码做额外的严格检查。
Partial (可选属性,但仍然不允许添加接口中没有的属性)
interface CourseGoal {
title: string;
description: string;
completeUntil: Date;
}
function createCourseGoal(
title: string,
description: string,
date: Date
): CourseGoal {
let courseGoal: Partial<CourseGoal> = {};
courseGoal.title = title;
courseGoal.description = description;
courseGoal.completeUntil = date;
return courseGoal as CourseGoal;
}
在这个例子当中,我们对 courseGoal
的类型写的是Partial<CourseGoal>
,意思是,结构和 CourseGoal 的结构一样,**但是!里面的每一个属性,都会是可选的,可以有也可以没有。**如果我们不这样写,下面的赋值语句会出错,因为我们最开始赋予 course Goal 的是一个空的 object。最后返回的时候,以 CourseGoal 返回即可。
Readonly (只读属性)
让一个变量只读
const names: Readonly<string[]> = ["Max", "Anna"];
names.pop();
pop 方法会报错,因为 Readonly
让这个变量只读。
Generic Types vs Union Type
为什么我们有时候需要泛型呢?看下面这个例子
class DataStorage<T extends string | number | boolean> {
//...
}
这是之前写的,如果我们不用泛型,使用 Union Type 呢?
class DataStorage {
private data: (string | number | boolean)[] = [];
addItem(item: string | number | boolean) {
//...
}
removeItem(item: string | number | boolean) {
//...
}
getItems() {
return [...this.data];
}
}
这样看好像可以,但实际上他的意思是,data 是可以存储三种类型的数组,下面的 method 参数,也都可以传三种。如果再换个方法呢?
class DataStorage {
private data: string[] | number[] | boolean[] = [];
// ...
}
这样好像可以,但是我们在传参数的时候。要判断他传的参数类型才可以做得到,因为 data 只能是一种的类型的 Array。