03-ts-泛型

602 阅读4分钟

这是我参与更文挑战的第4天,活动详情查看: 更文挑战


泛型

identity函数。 这个函数会返回任何传入它的值

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

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

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

我们给identity添加了类型变量T。 T帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。

定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:

let output = identity<string>("myString"); 

第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:

let output = identity("myString");

注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

泛型变量

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


function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

泛型类型

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

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: {<T>(arg: T): T} = identity;

这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口:

interface GenericIdentityFn {
    <T>(arg: T): T;
}

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

let myIdentity: GenericIdentityFn = identity;

一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: GenericIdentityFn<string>而不是GenericIdentityFn)。 这样接口里的其它成员也能知道这个参数的类型了。

interface GenericIdentityFn<T> {
    (arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;

当我们使用 GenericIdentityFn的时候,还得传入一个类型参数来指定泛型类型(这里是:number),锁定了之后代码里使用的类型。 对于描述哪部分类型属于泛型部分来说,理解何时把参数放在调用签名里和何时放在接口上是很有帮助的。

除了泛型接口,我们还可以创建泛型类。 注意,无法创建泛型枚举和泛型命名空间。

泛型约束

前面的例子,我们想访问arg的.length属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

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

现在我们把泛型约束一下,限制函数去处理任意带有.length属性的所有类型。只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。

改写如下:

interface Length {
    length: number
}

function loggingIdentity<T extends Length>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

现在这个泛型函数被约束了类型, 因此它不再适用于任意类型:

例如:

loggingIdentity(123) // Error, number doesn't have a .length property

我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({ length: 1, num: 1 })