在TypeScript中使用Rests和元组缩小函数参数类型

图片

TypeScript提供了多种方法来描述可以以多种方式调用的函数的类型。但是最常见的两种策略——函数重载和泛型函数——对于函数的内部实现如何理解其参数的类型并没有多大帮助。

本文将介绍三种技术,用于描述根据前一个参数变化的参数类型。前两种技术允许使用标准 JavaScript 语法,但在描述函数内部的类型时不够精确。第三种技术需要使用  ...  数组展开和  [...]  元组类型,以一种时髦的新方式在内部获得正确的类型。


依赖参数类型?怎么啦?

假设你想写一个函数,它接受两个参数:

  • fruit: either "apple" or "banana"

  • info :…

在这个函数中,第二个参数的类型与第一个参数不同。

一种初始化方法是将函数的两个参数 - fruit  和  info  声明为联合类型。简而言之:

 
declare function logFruit(  fruit: "apple" | "banana",  info: AppleInfo | BananaInfo): void;

这将允许使用正确的匹配类型调用函数,例如  "apple"  和一个匹配  AppleInfo  的对象。这可能看起来像这样:

 
interface AppleInfo {  color: "green" | "red";} interface BananaInfo {  curvature: number;} function logFruit(fruit: "apple" | "banana", info: AppleInfo | BananaInfo) {  switch (fruit) {    case "apple":      console.log(`My apple's color is ${(infoas AppleInfo).color}.`);      break;    case "banana":      console.log(        `My banana's curvature is ${(infoas BananaInfo).curvature}.`      );      break;  }} logFruit("apple", {color: "green" }); // Ok logFruit("banana", {color: "green" }); // Should error, but doesn‘t


代码块中存在两个问题:

  • 它需要手动  as  断言  info  是正确的类型,即使  fruit  的类型已经缩小,例如在  case "apple"  中。

  • 没有任何东西阻止调用类型不匹配的  logFruit ,例如  "banana"  和一个匹配  AppleInfo  的对象。

 logFruit  函数需要一种方法来表明  info  的类型取决于  fruit  的类型。


使用函数重载

描述函数有多种调用方式的一种方法是使用函数重载。为了概括函数重载,TypeScript允许代码在函数实现之前描述任意数量的调用签名。然后允许以匹配任何调用签名的方式调用函数。

例如,这个  eitherWay  可以用  number  输入来返回一个  number  ,或者用  string  输入来返回一个  string  :

 

function eitherWay(input: number): number;function eitherWay(input: string): string;function eitherWay(input: number | string) {  return input;}

eitherWay(123);

function eitherWay(input: number): number (+1 overload) 

eitherWay("abc");

function eitherWay(input: string): string (+1 overload)


用重载来描述  logFruit  函数——我们称之为  logFruitOverload ——看起来就像为  "apple"  和  "banana"  声明一个签名:

 

function logFruitOverload(fruit: "apple", info: AppleInfo): void;function logFruitOverload(fruit: "banana", info: BananaInfo): void;function logFruitOverload(  fruit: "apple" | "banana",  info: AppleInfo | BananaInfo) {  switch (fruit) {    case "apple":      console.log(`My apple's color is ${(infoas AppleInfo).color}.`);      break;    case "banana":      console.log(        `My banana's curvature is ${(infoas BananaInfo).curvature}.`      );      break;  }}

logFruitOverload("apple", {color: "green" }); // Ok

logFruitOverload("banana", {color: "green" }); // Should error

No overload matches this call.
Overload 1 of 2, '(fruit: "apple", info: AppleInfo): void', gave the following error.
Argument of type '"banana"' is not assignable to parameter of type '"apple"'.
Overload 2 of 2, '(fruit: "banana", info: BananaInfo): void', gave the following error.
Argument of type '{ color: string; }' is not assignable to parameter of type 'BananaInfo'.
Object literal may only specify known properties, and 'color' does not exist in type 'BananaInfo'.

这种函数重载方法确实比第一个方法在联合类型方面有所改进:

  • 用 "apple" 调用 logFruitOverload ,允许传递一个与 AppleInfo 匹配的对象作为 info 。

  • 使用  "banana"  和  AppleInfo  对象调用  logFruitOverload  是一个类型错误。

但是, case "apple":  中的  info  的类型仍然是  AppleInfo | BananaInfo 。您可以将光标或鼠标悬停在  info as AppleInfo  中的  info  上,以查看这一点。访问特定于水果的属性仍然需要显式的  as  断言。

TypeScript 不能缩小  info  的类型,即使两个重载明确声明每个水果字符串都匹配一个特定的接口。函数的实现签名 —— 它是其内部实现所遵循的 —— 不理解这种关系。


使用泛型函数

这个函数的另一种类型安全尝试可能是将其转换为泛型。  info  的类型取决于  fruit  的类型;因此,为  fruit  使用类型参数将允许根据  fruit  的类型收窄  info  的类型。


这个实现声明了一个  Fruit  类型参数,它必须是  InfoForFruit  接口的键之一 ( "apple" | "banana" ),然后声明  info  必须是那个  Fruit  键下的对应的  InfoForFruit  值:

 
interface InfoForFruit {  apple: AppleInfo;  banana: BananaInfo;} function logFruitGeneric<Fruit extends keyof InfoForFruit>(  fruit: Fruit,  info: InfoForFruit[Fruit]) {  switch (fruit) {    case "apple":      console.log(`My apple's color is ${(infoas AppleInfo).color}.`);      break;    case "banana":      console.log(        `My banana's curvature is ${(infoas BananaInfo).curvature}.`      );      break;  }}

logFruitGeneric("apple", {color: "green" }); // Ok

logFruitGeneric("banana", {color: "green" }); // Should error

Argument of type '{ color: string; }' is not assignable to parameter of type 'BananaInfo'.
Object literal may only specify known properties, and 'color' does not exist in type 'BananaInfo'.

这个通用版本在使用  "banana"  和错误信息形状的情况下,给出了一个友好的错误消息。但不幸的是,TypeScript 不能缩小  case "apple"  中的  info  的类型。


使用Rest参数
 

第三次也是最后一次尝试,让我们结合三段语法:

  1. 其余参数:使用  ...  允许任意数量的参数作为数组

  2. 元组类型:将数组的类型限制为固定大小,并为元素指定显式类型

  3. 数组解构赋值:将数组中的元素赋给局部变量

这个 logFruitTuple 版本使用了这三种技术,允许传入 fruit 和 info 参数,它们都属于 FruitAndInfo 元组类型:

 
type FruitAndInfo = ["apple", AppleInfo] | ["banana", BananaInfo]; function logFruitTuple(...[fruit, info]: FruitAndInfo) {  switch (fruit) {    case "apple":      console.log(`My apple's color is ${info.color}.`);      break;    case "banana":      console.log(`My banana's curvature is ${info.curvature}.`);      break;  }}

logFruitTuple("apple", {color: "green" }); // Ok

logFruitTuple("banana", {color: "green" }); // Should error

Argument of type '["banana", { color: "green"; }]' is not assignable to parameter of type 'FruitAndInfo'.
Type '["banana", { color: "green"; }]' is not assignable to type '["banana", BananaInfo]'.
Type at position 1 in source is not compatible with type at position 1 in target.
Type '{ color: "green"; }' is not assignable to type 'BananaInfo'.
Object literal may only specify known properties, and 'color' does not exist in type 'BananaInfo'.

这个函数根据  fruit  适当地缩小了  info  的类型,并用  "banana"  和一个  AppleInfo  形状的对象正确地标记了错误的调用。太棒了!

我们一步一步来,看这个参数是如何工作的:

  1. ...  是一个 rest 参数,将传递给函数的任何参数收集到一个数组中

  2. [fruit, info]  收集该数组的前两个元素,分别存储在  fruit  和  info  变量中。

  3. : FruitAndInfo  将参数的类型注释为  FruitAndInfo ,这是一个允许两个元组中的任何一个的并集:

把它们放在一起:函数接受两个参数,并把它们的类型一起赋给  FruitAndInfo 。这正是我们想要的!


它是如何工作的


TypeScript 很聪明,当元组的元素缩小时,该元组的其他元素也会缩小。这就是为什么 TypeScript 知道如果  fruit  是  logFruitTuple  中的  "apple" ,它可以将  info  的类型缩小为  AppleInfo 。

你可以把这种智能从函数参数中分离出来。在下面的代码片段中,当解构后的  fruit  变量被缩小为  "apple"  时, info  变量被缩小为  AppleInfo :

 

type FruitAndInfo = ["apple", AppleInfo] | ["banana", BananaInfo];

const [fruit, info]: FruitAndInfo = Math.random()

? ["apple", {color: "green" }]

: ["banana", {curvature: 35 }];

if (fruit=== "apple") {

info;

const info: AppleInfo

}


logFruitTuple  的  [fruit, info]  参数的类型为  FruitAndInfo ,因此它从元组类型缩小中受益。很聪明。


命名元组
 

元组方法的一个缺点是,默认情况下,参数没有名字。前面的  logFruitTuple  函数在编辑器中提示参数名时,使用自动生成的  __0_0  和  __0_  :

 
logFruitTuple(__0_0: "apple", __0_1: AppleInfo): void

你可以为元组类型的元素赋予显式的名称,从而解决这种命名行为。

 

type FruitAndInfo =  | [fruit: "apple", info: AppleInfo]  | [fruit: "banana", info: BananaInfo]; function logFruitTupleNamed(...[fruit, info]: FruitAndInfo) {  switch (fruit) {    case "apple":      console.log(`My apple's color is ${info.color}.`);      break;    case "banana":      console.log(`My banana's curvature is ${info.curvature}.`);      break;  }}

在元组中的每个元素前加上  fruit:  和  info:  ,会让 TypeScript 知道在函数中使用这些名称:

 
logFruitTuple(fruit: "apple", info: AppleInfo): void

参见 TypeScript 代码库issue上的 Default rest 参数名 以了解更多关于匿名元组解构名称的细节。



你应该使用哪一个?


视情况而定。


大多数情况下,这些复杂的函数签名是代码过于复杂的标志。在类型系统中难以表示的类型可能很难使用,因为它们在概念上很复杂。如果可能的话,尽量避免需要这种类型的函数签名。


技术权衡


如果你必须使用这些技术之一,考虑哪一种在将来会给你的项目开发人员带来最少的痛苦。许多 TypeScript 开发人员更喜欢避免函数重载,因为它们乍一看很古怪。泛型函数也可能吓到许多 TypeScript 开发人员,尽管在这种情况下,只有一个类型参数的泛型函数通常不会那么吓人。

...  其余元组参数是一个漂亮的技巧,但也冒着让开发人员感到困惑的风险。但是内部增加的类型安全性可能值得这种困惑。我的建议是等待一个它们非常适合的时间,尝试一次,看看它们的感觉如何。你可能会发现你喜欢它们,也可能不喜欢。

在数组中包装参数然后立即解构数组,如果使用在非常常见的运行路径中,也有损害运行时性能的小风险。尽管如此,在函数的大多数实例中,这种性能影响很可能被优化为零或接近零。在得出结论之前,始终要诊断运行时性能问题。


替代:使用对象


与其纠结于多个函数参数,不如考虑使用一个具有多个属性的对象。这样做将允许使用以下区别并集等技术来表示不同的允许形状。

 
interface AppleInfo {  color: "green" | "red";} interface AppleAndInfo {  fruit: "apple";  info: AppleInfo;} interface BananaInfo {  curvature: number;} interface BananaAndInfo {  fruit: "banana";  info: BananaInfo;} function logFruitTuple({fruit, info}: AppleAndInfo | BananaAndInfo) {  switch (fruit) {    case "apple":      console.log(`My apple's color is ${info.color}.`);      break;    case "banana":      console.log(`My banana's curvature is ${info.curvature}.`);      break;  }}

logFruitTuple({fruit: "apple", info: {color: "green" } }); // Ok

logFruitTuple({fruit: "banana", info: {color: "green" } }); // Should error

Type 'string' is not assignable to type '"green" | "red"'.

无论你选择哪种策略,记住要尽可能让代码可读、易懂。下面这段话来自Brian Kerninghan,适用于类型系统和运行时代码:

调试代码的难度是编写代码的两倍。 因此,如果你尽可能聪明地编写代码,那么根据定义,你不够聪明去调试它。

 欢迎关注公众号:文本魔术,了解更多

  • 52
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值