目录
前言
一、泛型值Hello World
二、使用泛型变量
三、泛型类型
四、泛型类
五、泛型约束
1、泛型约束
2、在泛型约束中使用类型参数
3、在泛型里使用类类型
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像C#和Java这样的语言中,可以使用泛型
来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
下面来创建第一个使用泛型的例子:indentity函数。这个函数会返回任何传入它的值。你可以把这个函数当成echo命令。
不用泛型的话,这个函数可能是下面这样:
function indentity(arg: number):number{
return arg;
}
或者我们使用any定义:
function indentity(arg:any):any{
return arg;
}
使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都可能被返回。
因此我们需要使用一种方法使返回值的类型与传入参数的类型是相同的。这里,我们使用了类型变量,它是一种特殊的变量,只用于表示类型而不表示值。
function indentity(arg: T):T{
return arg;
}
我们给函数添加一个类型变量T,T可以帮我们捕获输入的类型,之后我们就可以使用这个类型,当我们再次使用T当做返回值类型,现在我们可以知道参数类型与返回值类型是相同的了。这允许我们时刻跟踪函数里面的使用类型的信息。
我们把这种函数叫做泛型,因为它适用多个类型,不同于any,它不会丢失信息。
我们定义了泛型函数后,两种方法使用,第一种,传入所有的参数,包含类型参数:
let output = identity("myString");
这里我们明确的指定了T
是string
类型,并做为一个参数传给函数,使用了<>
括起来而不是()
。
第二种方法更常用,利用了类型推论--即编译器会根据传入的参数自动帮我们确定T的类型:
let output = identity("myString");
使用泛型函数的时候,编译器要求你的函数体必须是通用类型,你必须把参数当做 是任意或所有类型,比如:
function identity(arg: T) : T {
return arg;
}
假如我要打印出arg的长度,你可能会这样做:
function loggingIdentity(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
如果这么做,编译器会报错说我们使用了arg
的.length
属性,但是没有地方指明arg
具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length
属性的。
假如我们要操作T类型的数组,怎么办,当然有对应的创建泛型的类型:
function loggingIdentity(arg: T[]):T[]{
console.log(arg.length);//数组有长度的属性,所有不会报错
return arg;
}
前面两节说的是适用不同类型的泛型函数,这节我们探究的是函数本身,以及创建泛型接口。
泛型函数的类型与非泛型函数没什么不同,只是有一个类型参数在最前面,像函数声明一样:
function identity(arg: T): T {
return arg;
}
let myIdentity: (arg: T) => T = identity;
我们也可以使用不同的泛型参数名,只要在 数量上和使用方式对应上就可以。
function identity(arg: T): T {
return arg;
}
let myIdentity: (arg: U) => U = identity;
我们还可以使用带有调用签名的对象字面量来定义泛型函数:
function identity(arg: T): T {
return arg;
}
let myIdentity: {(arg: T): T = identity;}
这引导我们去写第一个泛型接口了,我们把上面的对象里面的字面量拿出来做一个接口:
interface GenericIdentityFn {
(arg: T): T;
}
function identity(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary
)。 这样接口里的其它成员也能知道这个参数的类型了。
interface GenericIdentityFn {
(arg: T): T;
}
function identity(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
不在描述泛型函数,而是把非泛型函数签名作为泛型类型一部分。当使用接口的时候,应传入一个类型参数指定泛型类型,锁定之后代码里使用类型。
泛型类看上去与泛型接口差不多,泛型类使用<>括起来泛型类型,跟在类名后面。
class GenericNumber {
zeroValue: T;
add:(x:T,y:T) =>T;
}
let MyGenericNumber = new GenericNumber();
MyGenericNumber.zeroValue=0;
MyGenericNumber.add = function(x,y){ return x+y; };
GenericNumber
类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number
类型。 也可以使用字符串或其它更复杂的类型。
let stringNumeric = new GenericNumber();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。
我们在学习类的时候学过,类有两部分:静态部分和实例部分,泛型类值得是实例部分的类型,所以静态属性不能使用这个泛型类型。
上面我们写过一个调用.length属性的函数,还有一种方法,就是定义一个接口,来描述约束函数的条件。创建一个包含.length属性的接口,使用这个接口和extends关键字来实现约束:
interface Lenghwise{
length: number;
}
function loggingIdentity(arg: T):T{
console.log(arg.length);
return arg;
}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型。
可以声明一个类型参数,且它被另一个类型参数约束。比如,现在我们想要用属性名对从对象里获取这个属性。并且我们想要确保这个属性存在于对象obj上,因此我们需要在这两个类型之间使用约束。
function getProperty(obj: T, key: K){
return obj[key];
}
let x = { a: 1,b: 2,c: 3,d: 4};
getProperty(x,"a");//正确
getproperty(x,"m");//错误
在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,
function create(c: {new() : T;}): T{
return new c();
}
一个更高级的例子,使用原形属性推断并约束构造函数与类实例的关系。
class BeeKeeper{
hasMask: boolean;
}
class ZooKeeper{
nametag: string;
}
class Animal{
numLegs:number;
}
class Bee extends Animal{
keeper:BeeKeeper;
}
class Lion extends Animal{
keeper:ZooKeeper;
}
function createInstance(c: new() => A):A{
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;