Hello World of Generics
让我们以范型的hello world:“identity方法:开始着手。identity方法是一个不论输入什么,输出都返回输入的方法。你可以把它看作和echo命令一样。

没有泛型的情况下,我们要没identity方法一个特地的类型:

function identity(arg: number): number {
    return arg;
}

或者,我们可以使用any类型来描述identity方法:
function identiry(arg: number): any{

return arg;

}
虽然使用any绝对是泛型,因为它使得这个方法可以接受任何类型。但是,当这个方法返回数据的时候,我们却丢失了返回值的类型。假如我们传入的是一个number类型,我们唯一的信息就是任何的类型都有可能被返回。

相反地,我们需要一种方法,这种方法既能获取参数的类型,又能标识返回类型。这里,我们需要用到类型变量,一种特殊的变量,用于类型而不是值。

function identity<T>(arg: T): T {
    return arg;
}

现在,我们已经给identity方法加了一个类型变量T。这个T允许我们获取用户提供的类型(例如:number),因为我们可以在稍后使用这个信息。这里,我们再次使用T作为返回类型。经考察,现在我们可以看到相同的类型被使用到了参数类型和返回类型。这允许我们一边传入类型信息,而在另一边传出类型信息。

我们说这个类型的identity方法是范型的,因为它适用于一系列的类型。和使用any不同,它和我们第一个使用number作为参数的方法一样精确(它不会丢失任何信息)。

一旦我们写好了identity方法,我们就可以以2种方式调用它。一种是传入所有的参数,包括类型参数:

let output = identity<string>('myString');//output的类型会是'string'

这里我们通过参数以<>而不是()围绕,显式地设置T的类型为string而作为方法调用的参数之一。

第二种方法或许是最常用的方法。这里我们使用类型参数推断-也就是我们想让编译器根据我们传入的参数类型来为我们推断出T的类型。

let output = identity('string');//output的类型是string

注意到我们不用显式地给尖括号(<>)里面传入类型;编译器只是查看'myString'的类型,然后把T设置为相同类型。虽然类型参数推断是一个使得我们的代码更短更可读的有用工具,但是在更复杂的例子里可能会发生编译器无法推断出类型的情况。在这种情况下,你就需要像我们在前一个例子里做的那样:显式地给类型参数传入类型。

使用范型变量
当你开始使用泛型,你会注意到当你创建一个类似于idenyity的范型方法的时候,编译器会强制你在这个方法体中正确地使用任何泛型参数。意思就是,你确实是把这些参数看作他们可以是任意类型。

让我们来看看之前的identity方法:

function identity<T>(arg: T): T {
    return arg;
}

假如我们想对每一次调用都打印参数的个数呢?或许我们会尝试这样写:

 function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

当我们这样做,编译器会因为我们使用了.arg的.length属性,但是我们却没有任何地方说明arg确实是有.length这个属性而给我们一个错误提示。请记住,就像我们早先说过的那样,这些类型参数代表任意的和所有的类型,因为使用这个方法的人很有可能传入一个number,但是number并没有.length属性。

比如说我们实际上是想让这个方法来处理T数组,而不是T。因为我们是处理数组,因此.length数组是可用的。我们可以就像创建其他类型的数组一样来创建T数组:

function loggingIdentity<T>(arg: T[]): T[]{
    console.log(arg.length);
    return arg;
}

你可以将loggingidentity的类型读取为,泛型方法loggingIdentity接受一个类型参数T和一个T类型数组的arg作为参数,并且返回一个T类型数组。假如我们传入一个number类型数组,那么我们就会得到一个number类型数组,因为T会绑定到number上。这允许我们使用泛型类型变量T作为我们正在使用的类型的一部分,而不是全部,从而给了我们更大的灵活性。
我们也可以换一种方式写我们上面的例子:

function loggingIdentity<T>(arg: Arrray<T>): Array<T>{
    console.log(arg.length);
    return arg;
}

接下来,我们将会学习如何创建你自己的如Array<T>这样的泛型类型。

泛型类型
在前面的部分,我们创建了适用于一系列类型的泛型方法。在这个部分,我们将探讨泛型方法本身的类型以及如何创建泛型接口。

泛型方法的类型和非泛型方法的类型一样,首先列出类型参数,和方法定义类似:

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <T>(arg: T) => T = identity;
在这个类型里,我们也可以给泛型类型参数一个别的名字,只要类型参数的数量以及使用方法一致就行:

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

我们也可以把泛型类型写作一个对象类型的调用签名:

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: {<T>(arg: T): T} = identity;

这就引导我们去写我们的第一个范型接口。让我们把上面例子里面的对象移到接口里面去:

interface GenericIdentityFn {
    <T>(arg: T):T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdetityFn = identity;

在一个相似的例子里,也许我们想把范型参数作为整个接口的参数。这让我们知道我们的范型覆盖了哪些类型(例如:Dictionary<string> 而不是Dictionary)。这就使得类型参数对接口的所有成员都是可见的了。

interface GenericIdentityFn<T> {
    (arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;

注意我们的例子有了略微的不同。比起描述一个范型方法,取而代之的是现在我们有了一个非泛型的方法签名作为范型类型的一部分。当我们使用GenericIdentityFn的时候,我们需要指定一个相应的类型参数(这里是:number),有效地锁定底层调用签名将要使用的东西。理解什么时候把类型直接放到调用签名上,什么时候放到接口本身,将有助于描述一个类型的哪些方面是范型的。

除了泛型接口,我们还可以创建泛型类。请注意创建范型枚举和范型命名空间是不可能的。

范型类


nanaistaken
586 声望43 粉丝