TypeScript 泛型
泛型(Generics)
软件工程的一个重要部分就是构建组件,组件不仅需要有定义良好和一致的API,也需要是可复用的。好的组件不仅能够兼容现在的数据类型,也能适用于未来可能出现的数据类型,这在构建大型软件系统时会给你最大的灵活度。 在比如C#和Java语言中,用来创建可复用组件的工具,我们称之为泛型。利用泛型,我们可以创建一个支持众多类型的组件,这让用户可以使用自己的类型消费这些组件。
泛型的使用
首先让我们来写一个泛型恒等函数,所谓恒等函数就是一个返回任何传进内容的函数。
function identity<T>(arg: T): T {
return arg;
}
const a = identity<number>(1); // const a: number
const b = identity<string>('zs'); // const b: string
const c = identity('true'); // 类型推断 const c: "true"
泛型函数的形式和其他非泛型函数的一样,都需要先列一个类型参数列表,这有点像函数声明:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <C>(arg: C) => C = identity;
泛型类型
结合函数中调用签名的方法,我们可以写出第一个泛型接口和泛型类型别名:
interface Fn {
<T>(arg: T): T;
}
type Fnn = {
<F>(arg: F): F
}
let identity: Fnn = function identity<D>(arg: D): D {
return arg;
}
const a = identity<number>(1); // const a: number
const b = identity<string>('zs'); // const b: string
const c = identity('true'); // 类型推断 const c: "true"
注意:泛型的类型参数可以使用不同的名字,只要数量和使用方式上一致就可以。 有些时候我们会希望泛型参数作为整个接口的参数,这让可以让我们清楚的知道传入的是什么参数,而且接口里其他成员也可以看到:
interface Fn<T> { // 泛型类型 or 泛型接口
(arg: T): T; // 非泛型函数
}
function identity<D>(arg: D): D {
return arg;
}
let myIdentity: Fn<string> = identity;
const a = myIdentity('zs')
在上面的例子中,我们只做了少许改动。不再描述一个泛型函数,而是将一个非泛型函数签名,作为泛型类型的一部分。
现在当我们使用Fn
时,需要给出明确的参数类型。
当要描述一个包含泛型的类型时,理解什么时候把类型参数放在调用签名里,什么时候把它放在接口里是很有用的。
除了泛型接口之外,我们也可以创建泛型类。注意,不能创建泛型枚举类型和泛型命名空间。
泛型类
泛型类写法上类似于泛型接口。在类名后面,使用尖括号中<>
包裹住类型参数列表:
class GenericNumber<NumType> {
zeroValue: NumType; //报错:属性“zeroValue”没有初始化表达式,且未在构造函数中明确赋值。
add: (x: NumType, y: NumType) => NumType; //报错:属性“add”没有初始化表达式,且未在构造函数中明确赋值。
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
上面官方文档给到的例子,照着敲下来发现会报错,查阅文档后发现,在TypeScript2.7+的版本中,加入了严格的类初始化检查strictPropertyInitialization
,该检查要确保类的每一个实例属性,都在声明时初始化或在构造函数中初始化。如果是简单类型还好,可以给一个初始值,但当我们使用泛型类初始化时,我们并不知道真正传给我们的是什么类型,所以此时可以用文档中给出的!
(确定的赋值断言),明确赋值断言是可以让我们的赋值操作放在实例属性声明之后,告诉TypeScript这个属性一定会赋值。官方文档给出的例子:
let x: number;
initialize();
console.log(x + x);
// ~ ~
// Error! 在赋值前使用了变量“x”。
function initialize() {
x = 10;
}
上面这段代码会抛出一个错误,原因跟我们泛型类声明时是一样的,此时我们就需要用到!
赋值断言:
let x!: number;
initialize();
console.log(x + x);// Ok
function initialize() {
x = 10;
}
那同样的我们在泛型类声明中也使用!
赋值断言:
class GenericNumber<NumType> {
zeroValue!: NumType;
add!: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
这样TypeScript就不会抛错了。 注意:类中不仅有实例成员,还有静态成员,但泛型类型只能对实例部分生效,静态成员是不可以使用泛型类型参数的。
class GenericNumber<NumType> {
zeroValue!: NumType;
add!: (x: NumType, y: NumType) => NumType;
static num: NumType; // 报错:静态成员不能引用类类型参数。
}
泛型约束
在之前学习函数类型时,就使用过泛型约束,我个人把它理解为类型范围的收窄,也就是约束泛型的类型范围,使得实际传入的类型必须有某一个必要条件。 在泛型约束中也可以使用泛型的类型参数,比如我们有一个函数,用来获取传入对象的某个属性,那我们首先要保证这个属性确实存在这个对象中。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let p = { name: 'zs', age: 18 };
getProperty(p, 'name'); // ok
getProperty(p, 'sex'); // 类型“"sex"”的参数不能赋给类型“"name" | "age"”的参数。
在泛型中使用类类型
在TypeSCript中,当使用工厂模式创建实例的时候,有必要通过他们的构造函数推断出类的类型,举个例子:
function create<Type>(c: { new (): Type }): Type {
return new c();
}
下面是一个更复杂的例子,使用原型属性推断和约束,构造函数和类实例的关系。
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();// Bee.keeper: BeeKeeper
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper(); //Lion.keeper: ZooKeeper
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; //function createInstance<Lion>(c: new () => Lion): Lion
createInstance(Bee).keeper.hasMask; // function createInstance<Bee>(c: new () => Bee): Bee