为何要使用泛型
1、在写一个API时,为追求后期的复用性、拓展性,往往希望其不仅可以支持当前类型,还能支持未来需要的类型,因此这个“类型”具体为何,希望是灵活的;
2、使用any大法虽然可以绕开各种类型检查,但是也丧失了本身类型约束的作用,例如我们往往希望输出与输入是保持类型一致的;
基于以上诉求,我们使用泛型函数,在支持多类型复用的同时避免信息的丢失,保证类型的准确性。以下为常规写法、使用any类型、使用泛型这三种写法:
function test_1(a: number) : number {
let res = a;
return res;
}
function test_2(a: any) {
let res = a ;
return res;
}
function testType<T>(a: T) : T {
let res = a;
return b;
}
泛型的定义
上文中的的函数test_3即我们说的泛型写法,其中尖括号中的T,代表的是一个变量,称之为 类型变量 ,作用是帮助我们进行类型追踪,以test_3这个函数为例,第一个尖括号中的T表示该函数仅有一个类型变量T,第二个T标识的是输入a的类型,第三个T标识要求的返回数据类型。因此,对于testType仅有一个入参,返回的数据类型需要与其一致。
注意其中的T仅是一个类型变量,我们也可以使用其他的标识方式,例如两个入参的case:
function testType<T>(a: T, b; U) : U {
return b;
}
该函数存在两个类型变量T和U,分别代表了第一、第二个入参的类型,而函数返回值的类型需要与第二个入参一致。
定义好泛型函数后,在使用的阶段,通常有指定类型和不指定类型两种写法,如下:
let output = testType<string>("adc"); // 通过尖括号指定类型为字符串
let output = testType("adc"); // 不指定类型,编译器类型推导得到其类型为字符串
泛型变量
泛型变量T不仅仅可以作为一整个类型,还可以作为类型的一部分,例如我们需要打印一个参数的长度:
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
或者
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
泛型接口
下面进行一个简单的泛型接口的创建:
interface GenericIdentityFn {
<T>(arg: T): T;
}
function testType<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = testType;
我们还可以把泛型接口中的泛型类型,作为一个独立的参数,使用泛型接口时需要传入指定的类型:
interface GenericIdentityFn<T> {
(arg: T): T;
}
function testType<T>(arg: T): T {
return arg;
}
let myIdentityByString: GenericIdentityFn<string> = testType;
let myIdentityByNum: GenericIdentityFn<number> = testType;
泛型类
与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。注意的是,我们无法创建泛型枚举、泛型命名空间。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
console.log(myGenericNumber.add(6, 6));
let myGenericString = new GenericNumber<string>();
myGenericString.zeroValue = 'a';
myGenericString.add = function(x, y) { return x + y; };
console.log(myGenericString.add('abc', 'defg'));
输出的值分别为 12 和 'abcdefg'
泛型约束
根据对泛型类型T的要求,定义某些约束条件,例如指定必须是number或者string类型:
// 添加泛型约束,需要是number或者string类型
function identity_<T extends number | string>(arg: T): T {
return arg;
}
还可以进行更复杂的约束。例如限制泛型类型必须含有某个属性:
interface LengthWise {
length: number
}
interface GenericIdentityFn<T> {
(arg: T): T;
}
// 添加泛型约束,必须含有length属性
function identity__<T extends LengthWise>(arg: T): T {
return arg;
}
// 此时传入number类型就会报错
let myIdentityhasLength: GenericIdentityFn<string> = identity__;
泛型约束拓展
下面介绍泛型约束中稍复杂一些的用法。
使用类型参数
// 泛型约束中使用类型参数
function getProperty<T, K extends keyof T>(obj: T, key: K) : T[K]{
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
使用类类型
使用ts写泛型工厂函数,引用构造函数的类类型:
interface CType<T> {
new(...arg: any): T;
}
// 定义类
class GenericBank {
a: number;
b: string;
constructor(public name :string) {};
}
// 常规创建
let test = new GenericBank('test1');
// 在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型
function create<T>(c: CType<T>, name: string): T {
return new c(name);
}
// 使用泛型工厂创建
let result = create<GenericBank>(GenericBank, 'test2');
console.log(test.name);
console.log(test.a);
console.log(result.name);
使用类型推断进行类型约束
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper; // 使用BeeKeeper原型推断,约束keeper的类型;
}
class Lion extends Animal {
keeper: ZooKeeper; // 使用ZooKeeper原型推断,约束keeper的类型;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!