2

我正在阅读 typescript 4.0 中的更改,它有一个名为Labeled Tuple Elements的部分。我的问题不是关于这个功能,我明白了,我得到了它的价值。我的问题是关于他们使用的示例:

函数 foo(...args: [string, number]): void { // ... }

…应该看起来和下面的函数没有什么不同…</p>

函数 foo(arg0: string, arg1: number): void { // ... }

问题是,以第一种方式定义函数有什么意义?据我所知,第二种方式客观上更好:相同的签名,更容易访问正文中的元素,自我记录(参数有名称)等。我错过了什么?第一个例子有什么价值?

我能想到的唯一优点就是可以参考args。如果您只是打算将其传递给另一个函数,这可能会有用吗?这就是你能做到这一点的原因吗?对于那种情况?

我知道这可以看作是基于意见的,但我正在寻找的答案不是:除了我提到的之外,你还能用这种语法做些什么我不理解的事情吗?

4

2 回答 2

3

我认为要理解的一件重要的事情是,打字稿的打字只是一个用于静态检查的类型层,它编译成完全不受类型系统影响的 javascript。如果所有东西都完全符合它们给出的类型,那么是的,除了将参数放在数组中之外,它...args: [string, number]基本上等同于。arg0: string, arg1: number

但是,如果不遵守类型(可能是由于不正确的转换或从 javascript 调用打字稿库),那么两者之间存在细微差别,因为第一个将处理任意数量的参数,并且数组将是适当的长度,而第二个将忽略第二个之后的任何参数并用 . 填充未指定的参数undefined

另一个更微妙的区别是其余参数不计入Function.prototype.length,这意味着您的第一个函数的长度为 0,而第二个函数正确报告的长度为 2。

这些差异真的很重要吗?可能不是。有什么理由使用第一个吗?除非您出于某种原因真的想要两个参数的数组,否则可能不会。第二个更具可读性,并允许您轻松添加默认参数值。为什么打字稿允许第一个?因为它应该是 javascript 的超集,并且 javascript 有剩余参数,所以 typescript 应该提供一种输入方式,即使长度有限。

于 2021-02-02T23:18:41.970 回答
2

其余元组语法允许您以常规调用签名不具备的方式抽象函数参数列表。如果您只打算使用一次抽象,那么它很少有用。想象一下,如果你被问到“JavaScript 中的函数有什么意义?” 并给出这个例子:

// why do this:
const val = ((x: number) => x * x)(5); 

// when the following is simpler:
const val = 5 * 5; 

当然,孤立地讲,没有理由创建一个类似 的函数x => x * x,调用它一次,然后将其丢弃。直接乘法显然更胜一筹。不过,这里的问题不在于功能,而在于示例。


因此,让我们看看休息元组以及一旦您被允许对参数列表进行抽象时它们可以使用的一些不同的东西:

在不同的函数类型中重用参数列表

假设您有一组函数,它们都采用相同的参数列表,但返回类型不同:

declare function foo(x: string, y: number, z: boolean): string;
declare function bar(x: string, y: number, z: boolean): number;
declare function baz(x: string, y: number, z: boolean): boolean;
// ... and maybe more

假设这不仅仅是巧合,而且函数以某种方式相关,您可能希望在某个时候更改参数列表,并且更愿意为每个函数执行一次而不是一次。使用其余元组,您可以这样做:

type Params = [x: string, y: number, z: boolean];
declare function foo(...args: Params): string;
declare function bar(...args: Params): number;
declare function baz(...args: Params): boolean;  

这是等效的。如果我想,比如说,添加第四个参数,我可以在Params.

参数列表的操作

假设我有相关的函数,它们有一些参数:

declare function qux(x: string, y: number, z: boolean): number
declare function quux(w: Date, x: string, y: number, z: boolean): string;

可能是因为quux()'s implementation 调用qux()。您可以重写quux()的签名以改用剩余元组:

declare function quux(w: Date, ...rest: Parameters<typeof qux>): string;

我正在使用Parameters实用程序类型qux函数中提取元组类型的参数。如果qux()' 的参数发生变化,则quux()' 的参数将自动更改。

参数列表的联合而不是重载

想象一下,我们有以下重载 frob()函数:

function frob(k: "number", val: number): void;
function frob(k: "string", val: string): void;
function frob(k: "number" | "string", val: number | string) {
  if (k === "number") {
    console.log(val.toFixed(1)); // error, oops
  } else {
    console.log(val.toUpperCase()); // error, oops
  }
}

作为呼叫者,您可以打电话frob("number", 123)或打电话,frob("string", "hello")但不能打电话frob("number", "hello")。但是,的实现frob()并不是那么好,因为编译器真的不明白k === "number"只有当val是 a时才会发生这种情况number。实现中的参数kval只是不相关的联合。所以你会得到一些错误。当然,这很容易解决类型断言

但请考虑以下版本的表现frob()力:

function frob(...args: [k: "number", val: number] | [k: "string", val: string]) {
  if (args[0] === "number") {
    console.log(args[1].toFixed(1)); // okay
  } else {
    console.log(args[1].toUpperCase()); // okay
  }
}

从调用者的角度来看,这与以前几乎相同。剩余元组的联合最终表现得像多个调用签名。但实现方式不同。编译器现在知道args其余参数是一个可区分的 union;如果第一个元素是"number",那么第二个元素肯定是 a number,编译器知道。所以它类型检查。

与参数列表相关的通用函数

考虑以下函数withConsoleMessage()。它接受一个函数作为输入,然后返回另一个函数,该函数string首先接受一个消息参数,然后是输入函数的所有参数。当您调用该函数时,它会记录消息并调用原始函数:

function withConsoleMessage<A extends any[], R>(
  cb: (...a: A) => R
): (msg: string, ...a: A) => R {
  return (msg, ...a) => (console.log(msg), cb(...a));
}

const atan2WithMessage = withConsoleMessage(Math.atan2);
const ret = atan2WithMessage("hello!", 4, 4); // "hello!"
console.log(ret * 4); // 3.141592653589793

看那个; 编译器知道atan2WithMessage()需要 astring然后是两个numbers。如果没有元组类型的剩余元素,就很难开始给出withConsoleMessage()类型签名。我想你可以给它一堆重载,这样它就可以接受不同参数的回调,直到某个任意限制:

function withConsoleMessage<R>(cb: () => R): (msg: string) => R;
function withConsoleMessage<R, A>(cb: (a: A) => R): (msg: string, a: A) => R;
function withConsoleMessage<R, A, B>(cb: (a: A, b: B) => R): (msg: string, a: A, b: B) => R;
function withConsoleMessage<R, A, B, C>(cb: (a: A, b: B, c: C) => R): (msg: string, a: A, b: B, c: C) => R;
function withConsoleMessage(cb: Function) {
  return (msg: string, ...a: any[]) => (console.log(msg), cb(...a));
}

这有点工作,在 TypeScript 3.0 之前,如果你想近似这种行为,这是你必须做的。与通用的休息元组相比,它又丑又脆。


我想我现在就到此为止;毕竟,我不知道我还要花多少时间来回答一个已经接受了不同答案的问题!可以说像 rest tuples 这样的抽象开辟了许多可能性

Playground 代码链接

于 2021-02-03T02:30:01.407 回答