9. 泛型

TS系列147728 阅读0

前面的是比较基础的,后面的对我来说就是进阶的了。想要玩转企业级开发,后面的东西必须认真对待了。

有些时候,函数返回值的类型与参数类型是相关的。

function getFirst(arr) {
return arr[0];
}

上面示例中,函数getFirst()总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。

这个时候只能用any来处理了

function f(arr:any[]):any {
return arr[0];
}

但是这样的标注,就反映不出参数与返回值之间的类型关系。就算返回一个'123'也不会报错。

为了解决这个问题,TypeScript 就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。

function getFirst<T>(arr:T[]):T {
return arr[0];
}

上面示例中,函数getFirst()的函数名后面尖括号的部分,就是类型参数,参数要放在一对尖括号(<>)里面。本例只有一个类型参数T,可以将其理解为类型声明需要的变量,需要在调用时传入具体的参数类型。

上例的函数getFirst()的参数类型是T[],返回值类型是T,就清楚地表示了两者之间的关系。比如,输入的参数类型是number[],那么 T 的值就是number,因此返回值类型也是number。

这里的T很像数学中的未知数x。函数接收一个x组成的数组,返回的元素需要为其中一个x,当然这里指的是类型。

这样定义了方法之后,在使用的时候就需要传入一个类型:

getFirst<number>([1, 2, 3])

调用函数getFirst()时,需要在函数名后面使用尖括号,给出类型参数T的值,本例是。但是往往可以省略不写类型参数的值,让 TypeScript 自己推断。

getFirst([1, 2, 3])

有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出了。

function comb<T>(arr1:T[], arr2:T[]):T[] {
return arr1.concat(arr2);
}
comb([1, 2], ['a', 'b']) // 报错
comb<number|string>([1, 2], ['a', 'b']) // 正确

类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用T(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。

function map<T, U>(
arr:T[],
f:(arg:T) => U
):U[] {
return arr.map(f);
}

// 用法实例
map<string, number>(
['1', '2', '3'],
(n) => parseInt(n)
); // 返回 [1, 2, 3]

总之,泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。

泛型主要用在四个场合:函数、接口、类和别名。

函数的泛型写法

function关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面。

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

那么对于变量形式定义的函数,泛型有下面两种写法。

// 写法一
let myId:<T>(arg:T) => T = id;

// 写法二
let myId:{ <T>(arg:T): T } = id;

接口的泛型写法

interface 也可以采用泛型的写法。

interface Box<T> {
contents: T;
}

let box:Box<string>;

类的泛型写法

泛型类的类型参数写在类名后面。

class Pair<K, V> {
key: K;
value: V;
}

类型别名的泛型写法

type Nullable<T> = T | undefined | null;

type Container<T> = { value: T };
const a: Container<number> = { value: 0 };
const b: Container<string> = { value: 'b' };

类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。

function getFirst<T = string>(
arr:T[]
):T {
return arr[0];
}

但是,因为 TypeScript 会从实际参数推断出T的值,从而覆盖掉默认值,所以下面的代码不会报错。

getFirst([1, 2, 3]) // 正确

一旦类型参数有默认值,就表示它是可选参数。如果有多个类型参数,可选参数必须在必选参数之后。

数组的标注:arr:string[]也可以使用泛型表示:arr:Array。TypeScript 默认还提供一个ReadonlyArray接口,表示只读数组。

类型参数的约束条件

function comp<Type>(a:Type, b:Type) {
if (a.length >= b.length) {
return a;
}
return b;
}

上面示例中,类型参数 Type 有一个隐藏的约束条件:它必须存在length属性。如果不满足这个条件,就会报错。

TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明。

function comp<T extends { length: number }>(
a: T,
b: T
) {
if (a.length >= b.length) {
return a;
}
return b;
}

comp([1, 2], [1, 2, 3]) // 正确
comp('ab', 'abc') // 正确
comp(1, 2) // 报错

注意

尽量少用泛型。泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。一般来说,只要使用了泛型,类型声明通常都不太易读,容易写得很复杂。因此,可以不用泛型就不要用。

评论

发表评论