泛型
其实 TypeScript 的本质,是在对类型进行编程?
这门编程语言的本质,即 TypeScript 在 JavaScript 对值进行编程的能力之上,又给予了你对类型进行编程的能力。为什么我们需要对类型进行编程?当然是因为,有时候类型世界也存在着和实际值一致的逻辑,就像我们已经学习的联合类型与交叉类型,就很好地证明了这一点。
在绝大部分编程语言中,函数都是一个非常重要的概念,如果缺少了函数,我们的代码可能会变得冗长晦涩,到处夹杂着重复的片段。
而在函数中,最重要的概念则是参数,参数是一个函数向外界开放的唯一入口,随着入参的差异,函数可能也会表现出各不相同的行为。
这一节我们要学习的概念「泛型」
,其实本质就是类型世界中的参数。
上一节我们学习到了类型别名,提到了类型别名能够充当一个变量,存放一组存在关联的类型:
type Status = 'success' | 'failure' | 'pending';
其实类型别名还能够充当函数的作用,但函数怎么能没有入参?我们可以这么来为类型别名添加一个入参,也就是泛型:
type Status<T> = 'success' | 'failure' | 'pending' | T;
type CompleteStatus = Status<'offline'>;
这里的 CompleteStatus
,其实等价于:
type CompleteStatus = 'success' | 'failure' | 'pending' | 'offline';
在这个例子中,Status
就像一个函数,它声明了自己有一个参数 T
,即泛型
,并会将这个参数 T 合并到自己内部的联合类型中。我们可以用一段伪代码来理解:
function Status(T){
return ['success', 'failure', 'pending', T]
}
const CompleteStatus = Status('offline');
很容易发现,这里的泛型就是参数作用,只不过它接受的是一个类型而不是值,同时,我们可以把联合类型类比为类型的集合。
在 TypeScript 中,变量与函数都由类型别名来承担,而一个类型别名一旦声明了泛型,就会化身成为函数,此时严格来说我们应该称它为「工具类型」
。
看起来好像类型别名才是主角,泛型的存在感还比较弱,它就是一个默默无闻的参数罢了?那是因为我们这里展示的是「主动赋值」
的用法.
而实际上,「自动推导」
才是泛型的强大之处所在。
我们先回到 JavaScript 中的函数,想象我们有一个这样的函数,它的出参与入参类型是完全一致的,比如给我个字符串,我就返回字符串类型,如果是数字,就返回数字类型,此时你会怎么对这个函数进行精确地类型标注,联合类型吗?
function factory(input: string | number): string | number {
// ...
}
首先,这么做会导致你丢失「出参与入参类型完全一致」这个信息,在你使用这个函数时,它只会提醒你返回值可能有字符串和函数,而不会根据你当前的入参给出唯一匹配的那个出参。
其次,假设随着需求变更,可能的入参又多了一个布尔值类型,难不成你又要再加一次?一次两次还好,如果后面慢慢到十几个类型,你猜同事看见你的代码会是什么心情?
这个时候我们就要请出泛型了,我们前面是把一个类型主动赋值给泛型,而其实人家真正的作用可不仅于此,我们先给这个函数添加上泛型:
function factory<T>(input: T): T {
// ...
}
可以看到这里我们一共出现了三个 T,它们的作用分别是什么?
首先,类似于类型别名中,<T>
是声明了一个泛型,而参数类型与返回值类型标注中的 T 就是普通的类型标注了
这里的整体意思其实是:这个函数有一个泛型 T,当你的函数获得一个入参时,会根据这个入参的类型自动来给 T 赋值,然后同时作为入参与返回值的实际类型!
“自动赋值”以及“同时作为入参与返回值的实际类型”
- 前者意味着我们无需再操心到底会有哪些可能的类型输入了
- 后者意味着我们只需要在两处使用同一个泛型参数,就实现了入参与返回值的类型绑定
你可能会想,一个泛型参数不一定够用啊,万一我可能有多个参数都需要填充泛型,但只有其中的一个泛型参数会被作为返回值类型呢?没问题,我们来声明多个泛型看看:
function factory<T1, T2, T3>(input: T1, arg1: T2, arg2: T3): T1 {
// ...
}
类似于上面的例子,在你给这些参数赋值时,泛型参数 T1
T2
T3
会被分别进行赋值,而只有泛型 T1
会被作为返回值的参数。
这个例子只是为了向你展示如何提供多个泛型参数,本质上它只需要一个泛型参数即可——为什么?
我们定义泛型参数是为了在未来的某一刻消费它,比如函数内部的逻辑,比如返回值的类型,在这里只有 T1
参数得到了应用,而 T2
T3
虽然会被填充,但却没有用武之地。
因此,切记不要为了使用泛型而使用泛型,确保只在你需要进行上面例子中那样参数与返回值类型的关联时,才使用泛型。