TypeScript中的难点:<泛型>
为什么使用泛型
假如我们有这样一个程序:
class KeyValue {
constructor(public key: number, public value: string) {}
}
let keyValue = new KeyValue(1, "kevin");
那么如果我们想让 key 传入的是 string 类型呢?
我们可能会想到新建一个类,或者使用联合类型,或者使用 any 类型。
但这不是我们应该做的,新建一个类会写重复的代码。使用联合类型不仅会得不到正确的代码补全,并且当 key 为新类型时,还需要在原有代码上增添新类型。使用 any 类型会让我们失去使用 ts 的意义。
这时候我们应该用到泛型(generic)。
在类中使用泛型
class KeyValue<T> {
constructor(public key: T, public value: string) {}
}
let keyValue1 = new KeyValue<number>(1, "kevin");
let keyValue2 = new KeyValue<string>("2", "kevin");
当我们使用泛型时,我们应该先声明,声明的方法就是在类名后使用尖括号<>
然后在里面声明一个或多个类型变量,当声明多个泛型时使用逗号隔开。这个变量可以是任意字母。
不过我们通常使用大写字母开头,并且喜欢使用单个字母。很多时候你会看到T
,实际上这来源于 c++的 temple 模板类。
当我们使用的时候会向上面那样传入类型,实际上我们传入类型时 ts 也会进行自动推断类型。这时候我们没有多写代码,同时我们也得到了安全的类型以及智能的代码补全。
现在我们将这个需要实现可复用的类进行完善:
class KeyValue<K, V> {
constructor(public key: K, public value: V) {}
}
let keyValue1 = new KeyValue<string, boolean>("2", true);
let keyValue2 = new KeyValue(1, "kevin");
在函数和方法中使用泛型
在函数中使用:
function wrapInArray<T>(value: T) {
return [value];
}
let numebrs = wrapInArray("1");
let numebrs1 = wrapInArray(1);
在方法中使用:
class ArrayUtils {
wrapInArray<T>(value: T) {
return [value];
}
}
let utils = new ArrayUtils();
let numbers = utils.wrapInArray(1);
接口泛型
interface Result<T> {
data: T;
}
假如我们想要像下面那样,使用接口描述一个请求的返回值:
interface Result<T> {
data: T | null;
error: null | string;
}
function fetch<T>(url: string): Result<T> {
return { data: null, error: null };
}
我们可以这样写:
interface Result<T> {
data: T | null;
error: null | string;
}
function fetch<T>(): Result<T> {
return { data: null, error: null };
}
interface User {
username: string;
}
interface Product {
title: string;
}
let result = fetch<Product>();
result.data?.title;
这时候当我们写result.data
时 ts 会根据我们传入的类型来进行代码补全的提示。
比如我们这里传入的 Product 接口,那么这时返回值中的 data 的类型为 Product,一个只有属性名为title
的对象,所以会提示我们输入 title。至于为什么是可选属性,因为 data 的属性为 T|null 可能为 null 类型。
限制泛型类型
有时候我们需要在构造函数中使用泛型,现在让我们写一个简单的函数
function echo<T>(value: T): T {
return value;
}
echo("1");
我们可以发现我们能向 echo 中传入任何类型的值,我们可以使用 extends 来限制它。这样只能传入继承的类型。
就像这样:
function echo<T extends string | number>(value: T): T {
return value;
}
echo("1");
echo(1);
//echo(true); //报错:类型“boolean”的参数不能赋给类型“string | number”的参数。ts(2345)
我们也可以使泛型继承一个接口:
interface Person {
name: string;
}
function echo<T extends Person>(value: T): T {
return value;
}
echo({ name: "kevin" });
我们也可以使用类来限制泛型:
class Person {
constructor(public name: string) {}
}
class Customer extends Person {}
function echo<T extends Person>(value: T): T {
return value;
}
echo({ name: "kevin" });
echo(new Person("person"));
echo(new Customer("customer"));
传入的值可以是符合类型的对象,实例对象或者子类的实例对象。
泛型类和继承
现在我们有这么段代码:
假如我们有一个商店类,使用 add 方法添加产品。为了让 object 不被外部调用,我们声明其为私有属性,向开头添加下划线_
并且使其数组初始化为空数组。
interface Product {
name: string;
price: number;
}
class Store<T> {
private _object: T[] = [];
add(obj: T): void {
this._object.push(obj);
}
}
接下来我们会通过三种场景来扩展这个类
传递泛型 T
interface Product {
name: string;
price: number;
}
class Store<T> {
private _object: T[] = [];
add(obj: T): void {
this._object.push(obj);
}
}
class CompressibleStore<T> extends Store<T> {
compress() {}
}
let store = new CompressibleStore<Product>();
store.compress();
store.add({ name: "nice", price: 22 });
我们在继承一个声明了泛型的类时,我们这个类也需要声明泛型,因为在这里T
作为的是值,而类型变量T
在这里还没有声明,所以我们需要先声明它。
限制泛型类型
我们新声明了一个提供查找方法的类。
我们在这个类中提供了 find 方法,然后想要使用 Store 的_object
属性,所以我们将修饰词改为protected
。数组的 find 方法如果找到了这个元素会返回元素本身,如果没有找到则返回 undefined,所以我们的 find 方法返回值为T | undefined
interface Product {
name: string;
price: number;
}
class Store<T> {
protected _object: T[] = [];
add(obj: T): void {
this._object.push(obj);
}
}
class CompressibleStore<T> extends Store<T> {
compress() {}
}
class SearchableStore<T extends { name: string }> extends Store<T> {
find(name: string): T | undefined {
return this._object.find((obj) => obj.name === name);
}
}
new SearchableStore<Product>();
new SearchableStore<{ name: string }>();
混合泛型类型属性
也就是在继承时直接传入泛型
interface Product {
name: string;
price: number;
}
class Store<T> {
protected _object: T[] = [];
add(obj: T): void {
this._object.push(obj);
}
}
// 1、传递泛型T
class CompressibleStore<T> extends Store<T> {
compress() {}
}
// 2、限制泛型类型
class SearchableStore<T extends { name: string }> extends Store<T> {
find(name: string): T | undefined {
return this._object.find((obj) => obj.name === name);
}
}
// 3、修复泛型类型属性
class ProductStore extends Store<Product> {
filterByCategory(): Product[] {
return [];
}
}
泛型与键操作符
假设我们在 Store 中声明了 find 方法,希望能查找 obj。
我们的 property 为键,类型为 string,value 为值,类型为 unknown,因为我们不知道值为什么类型。返回值为 T(obj 的类型)或者 undefined(没有找到)。
我们让 find 的条件为obj[property] === value
这时会报错(元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "unknown"。在类型 "unknown" 上找不到具有类型为 "string" 的参数的索引签名。ts(7053))。
因为在使用obj[property]
时 ts 会认为我们在使用索引签名。我们处理的只是 T 类型的实质类型。所以我们应该将property
的类型改为keyof T
,如果传入 Product,这代表类型为'name' | 'price'。
这时我们传入 property 的值只能为 name 或者 price 了。
interface Product {
name: string;
price: number;
}
class Store<T> {
protected _object: T[] = [];
add(obj: T): void {
this._object.push(obj);
}
// T is Product
// keyof T => 'name' | 'price'
find(property: keyof T, value: unknown): T | undefined {
return this._object.find((obj) => obj[property] === value);
}
}
let store = new Store<Product>();
store.add({ name: "kevin", price: 2 });
store.find("name", "kevin");
store.find("price", 2);
// store.find("nonExistingProperty", 2); //类型“"nonExistingProperty"”的参数不能赋给类型“keyof Product”的参数。ts(2345)
类型映射
如果我们现在要添加一个接口,它的属性和上面的 Product 一样,只是属性被修饰为 readonly,我们可能会这样写:
interface Product {
name: string;
price: number;
}
interface ReadonlyProduct {
readonly name: string;
readonly price: number;
}
但是我们不应该写这么重复的代码,我们应该使用类型映射:
interface Product {
name: string;
price: number;
}
type ReadonlyProduct = {
readonly [K in keyof Product]: Product[K];
};
类型映射应该使用type
声明,并且我们通过索引签名和 for in 循环来声明属性和类型。并且在属性前添加 readonly 关键字。
interface Product {
name: string;
price: number;
}
type ReadonlyProduct = {
readonly [K in keyof Product]: Product[K];
};
let product: ReadonlyProduct = {
name: "k",
price: 2,
};
// product.name = "kevin"; //无法分配到 "name" ,因为它是只读属性。ts(2540)
如果我们希望其他的接口也是只读的,我们可以使用泛型:
interface Product {
name: string;
price: number;
}
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
let product: ReadOnly<Product> = {
name: "k",
price: 2,
};
// product.name = "kevin"; //无法分配到 "name" ,因为它是只读属性。ts(2540)
现在我们根据上面的经验,创建一个可选属性的类型映射和可能为 null 类型的类型映射:
type Optional<T> = {
[K in keyof T]?: T[K];
};
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
这些类型映射是非常有用的,实际上 ts 内置了这些类型映射(工具类型TypeScript: Documentation - Utility Types (typescriptlang.org))