Skip to content

泛型

泛型有两个作用 一是在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型。适用于声明时不能确定类型,使用时才能确定类型。 二是约束某个类型必须有次功能

ts
interface IClass <T> {
    new (name: string): T
}

const createInstance<T> = (class: IClass<T>, name: string) => {
    return new class(name)
}

const i = createInstance<Person>(Person, 'foo')
// 调用时 <> 内传入了类型,传给函数后,函数又传给接口。

函数中使用泛型

ts

createArray

实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:

typescript
function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray<string>(3, 'x');

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

typescript
createArray<string>(3, 'x');

第二种方法更普遍。利用了类型推论,让类型推论自动推算出来,帮助我们保持代码精简和高可读性。 如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下, 这是可能出现的。

typescript
createArray(3, 'x');

多个类型参数

定义泛型的时候,可以定义多个类型参数

typescript
function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

// 写在函数上的泛型表示调用函数时,传入具体类型。
interface Swap {
    <T, U>(tuple: [T, U]): [U, T]
}
const swap: Swap = <T, U>(tuple: [T, U]): [U, T] => {
    return [tuple[1], tuple[0]];
}

// 写在接口上的泛型,表示使用接口时传入类型。
interface Swap<T, U> {
    (tuple: [T, U]): [U, T]
}
const swap: Swap:<string, number> = <T, U>(tuple: [T, U]): [U, T] => {
    return [tuple[1], tuple[0]];
}

泛型约束

ts
// 只要有 length 就相加
function sum = <T extends { length: number }>(a: T, b: T): T => {
    return (a + b) as T
}

sum('123', [1])

默认泛型

ts
interface O<T=string> {
    name: T
}
type T1 = O
type T2 = O<number>
type T3 = O<boolean>
let o: T1 = {
    name: 'string'
}
let o2:T2 = {
    name: 1
}

使用泛型变量

使用泛型创建像identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。

typescript
function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error,arg不一定包含属性 length,所以编译的时候报错了。
    return arg;
}

如果这么做,编译器会报错说我们使用了arg的.length属性,但是没有地方指明arg具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length属性的。

现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

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

你可以这样理解loggingIdentity的类型:泛型函数loggingIdentity,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时 T的的类型为number。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。

我们也可以这样实现上面的例子:

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

泛型约束

typescript
// 使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。
// 如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了:
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}
ts
// keyof 可以取出对象中的属性
const getVal = <T extends Object,K extends keyof T>(target: T, key: K) => {

}

getVal({ a: 1, b: 2 }, 'a')

type T1 = keyof any // key 可以是 number、string、symbol
type T2 = keyof (string | number)

泛型修饰类

ts
class Ary<T> {
    ary: T[] = []

    add (v: T) {
        this.ary.push(v)
    }
}

多个类型参数之间也可以互相约束:

typescript
// 使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。
function copyFields<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = (<T>source)[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 });

泛型接口

可以使用带有调用签名的对象字面量来定义泛型函数:

typescript
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

let createArray: {<T>(length: number, value: T): Array<T>} = createArray;

把上面例子里的对象字面量拿出来做为一个接口:

typescript
interface CreateArrayFunc {
    <T>(length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型,这样接口里的其它成员也能知道这个参数的类型了。

typescript
interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

// 此时在使用泛型接口的时候,需要定义泛型的类型。
let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

泛型类

泛型类看上去与泛型接口差不多。 泛型类使用 <> 括起泛型类型,跟在类名后面。

typescript
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; };

GenericNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。

typescript
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。

我们在类那节说过,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

泛型参数的默认类型

可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

typescript
function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}