TS泛型,拿下!

190 阅读7分钟

what is 泛型?

泛型程序设计是一种编程风格或编程范式,它允许在程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数。 看个示例就秒懂了:

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

const foo = identify('foo'); // 可以推断出 foo 的类型是 'foo'
const bar = identify(true); // 能够推断出bar的类型为true

tips:这就定义了一个泛型函数,它的参数类型和返回值类型由T的实例类型决定

形式类型参数

设置默认类型

下例中形式类型参数T的默认类型为boolean类型

<T = boolean>

tips:类型参数的默认类型也可以引用形式类型参数列表中的其他类型参数,但是只能引用在当前类型参数左侧(前面)定义的类型参数

<T, U = T>

可选的类型参数

如果一个形式类型参数没有定义默认类型,那么它是一个必选类型参数;反之,如果一个形式类型参数定义了默认类型,那么它是一个可选的类型参数。在形式类型参数列表中,必选类型参数不允许出现在可选类型参数之后。示例如下:

<T = boolean, U> // 错误
<T, U = boolean> // 正确

若一个类型参数的默认类型引用了其左侧声明的类型参数,则没有问题;若一个类型参数的默认类型引用了其右侧声明的类型参数,则会产生编译错误,因为此时引用的类型参数处于未定义的状态。示例如下:

<T = U, U = boolean> // 错误
<T = boolean, U = T> // 正确

实际类型参数

当显式地传入实际类型参数时,只有必选类型参数是一定要提供的,可选类型参数可以被省略,这时可选类型参数将使用其默认类型。示例如下:

function f<T, U = boolean>() {}

f<string>(); // 省略可选类型参数

f<string, string>();

泛型约束

基本使用

在泛型的形式类型参数上允许定义一个约束条件,它能够限定类型参数的实际类型的最大范围。我们将类型参数的约束条件称为泛型约束。示例如下:

interface point {
    x: number,
    y: string
}

function getPoint<T extends point>(args: T): T {
    return args
}

console.log(getPoint({x: 123, y: 456})); // 错误
console.log(getPoint({x: 123, y: '456'})); // 正确
console.log(getPoint({x: 123, y: '456', z: 678})); // 正确

对于一个形式类型参数,可以同时定义泛型约束和默认类型,但默认类型必须满足泛型约束。示例如下:

<T extends number = 0 | 1>

例如,下例中类型参数T的泛型约束为number类型,默认类型为数字字面量类型的联合类型“0 | 1”

泛型约束引用类型参数

在泛型约束中,约束类型允许引用当前形式类型参数列表中的其他类型参数。例如,下例中形式类型参数U引用了在其左侧定义的形式类型参数T作为约束类型:

<T, U extends T>
<T extends U, U>

需要注意的是,一个形式类型参数不允许直接或间接地将其自身作为约束类型,否则将产生循环引用的编译错误。例如,下例中的泛型约束定义都是错误的:

<T extends T> // 错误
<T extends U, U extends T> // 错误

基数约

如果类型参数T没有声明泛型约束,那么类型参数T的基约束为空对象类型字面量“{}”。除了undefined类型和null类型外,其他任何类型都可以赋值给空对象类型字面量。示例如下:

<T> // 类型参数T的基数约为“{}”类型

常见错误

interface point {
    x: number,
    y: string
}

// 编译错误:不能将类型“{ x: number; y: string; }”分配给类型“T”。
function getPoint<T extends point>(args: T): T {
    return {
        x: 123,
        y: '456'
    }
}

tips:传进来的 args 可能是 {x: 456, y: '789'} 类型,但是这里限定传进来和返回的应该是一样的,也就是应该返回 args,所以出现错误(当然你可以通过类型断言去躲避编译器检查)

泛型函数

若一个函数的函数签名中带有类型参数,那么它是一个泛型函数。泛型函数中的类型参数用来描述不同参数之间以及参数和函数返回值之间的关系。泛型函数中的类型参数既可以用于形式参数的类型,也可以用于函数返回值类型。

  • f1函数接受两个相同类型的参数,函数返回值类型是数组并且数组元素类型与参数类型相同。
function f1<T>(x: T, y: T): T[] {
    return [x, y]
}

const a = f1(123, 456)
const b = f1('123', '456')
  • f2函数接受两个不同类型的参数,并且返回值类型为对象类型。返回值对象类型中x属性的类型与参数x类型相同,y属性的类型与参数y类型相同。
function f2<T, U>(x: T,  y: U): { x: T, y: U} {
    return { x, y }
}

const a = f2('123', 456)
const b = f2(123, '456')
  • f3函数接受两个参数,参数a为任意类型的数组;参数f是一个函数,该函数的参数类型与参数a的类型相同,并返回任意类型。f3函数的返回值类型为参数f返回值类型的数组。
function f3<T, U>(a: T[], f: (x: T) => U): U[] {
    return a.map(f);
}

const a: boolean[] = f3<number, boolean>([1, 2, 3], n => !! n)

泛型函数类型推断

在大部分情况下,TypeScript编译器能够自动推断出泛型函数的实际类型参数。如果在上例中没有传入实际类型参数,编译器也能够推断出实际类型参数,甚至比显式指定实际类型参数更加精确。示例如下:

function f0<T>(x: T): T {
    return x
}

const a = f0(123) // 推断出类型为 123
const b = f0('123') // 推断出类型为 '123'

此例中编译器推断出的实际类型参数不是number 和 string类型,而是字符串字面量类型123和“123”。因为TypeScript有一个原则,始终将字面量视为字面量类型,只在必要的时候才会将字面量类型放宽为某种基础类型,例如string类型。此例中,字符串字面量类型“123”是比string类型更加精确的类型。在实际使用中,我们也正是希望编译器能够尽可能地帮助细化类型。

如果一个类型参数只在函数签名中出现一次,则说明它与其他值没有关联,因此不需要使用类型参数,直接声明实际类型即可。从技术上讲,几乎任何函数都可以声明为泛型函数。若泛型函数的类型参数不表示参数之间或参数与返回值之间的某种关系,那么使用泛型函数可能是一种反模式。

// 没必要使用泛型
function f<T>(x: T): void {
    console.log(x)
}

// 直接限定就好了
function f(x: number): void {
    console.log(x)
}

泛型接口

使用泛型是声明数组类型的两种方式之一,例如“Array”。“Array”是TypeScript内置的泛型数组类型,它的定义如下所示(从TypeScript源码中摘取部分代码):

interface Array<T> {
    pop(): T | undefined;
    push(...items: T[]): number;
    reverse(): T[];
    [n: number]: T;
}

泛型类型别名

在泛型类型别名定义中,形式类型参数列表紧随类型别名的名字之后。泛型类型别名定义的语法如下所示:

type Nullable<T> = T | undefind | null

示例1 使用泛型类型别名定义简单容器类型,如下所示:

type Container<T> = {value: T}

const a: Container<string> = { value: '123' }

示例2 使用泛型类型别名定义树形结构,如下所示:

type Tree<T> = {
    value: T,
    left: Tree<T> | null,
    right: Tree<T> | null
}

const tree: Tree<number> = {
    value: 123,
    left: {
        value: 456,
        left: null,
        right: null
    },
    right: {
        value: 456,
        left: null,
        right: null
    },
}

console.log(tree);

泛型类

在泛型类定义中,形式类型参数列表紧随类名之后。定义泛型类的语法如下所示:

class Container<T> {
    constructor(private readonly a: T) {}
}

const a = new Container<number>(123)

console.log(a);
// 表达式写法
const theClass = class Container<T> {
    constructor(private readonly a: T) {}
}

const a = new theClass<number>(123)

console.log(a);

泛型类中的类型参数允许在类的继承语句和接口实现语句中使用,即extends语句和implements语句。例如,下例中分别定义了泛型接口A和泛型类Base、Derived。在泛型类Derived中定义的类型参数T允许在基类和实现的接口中引用。示例如下:

interface A<T> {
    a: T
}

class Base<T> {
    b?: T
}

class Derived<T> extends Base<T> implements A<T>{
    constructor(public readonly a: T) { 
        super()
    }
}

const a = new Derived<number>(123)

console.log(a);