学习TypeScript:变量元组类型预览

192 阅读5分钟

TypeScript 4.0应该会在2020年8月发布,而这个版本最大的变化之一就是变量元组类型。尽管在写这篇文章的时候,他的功能还很热,但还是值得一看,看看我们能用它做什么。请注意,这里的东西可能会有变化,所以要谨慎!在4.0进入RC或发布之前,我将努力保持这个页面的更新。

如果你想自己试试,你可以把早期版本的分支加载到TypeScript的操场上

变体元组#

TypeScript中的元组类型是一个具有以下特征的数组。

  1. 数组的长度被定义。
  2. 每个元素的类型是已知的(而且不一定是相同的)。

例如,这是一个元组类型。

type PersonProps = [string, number]

const [name, age]: PersonProps = ['Stefan', 37]

变体元组类型是一种具有相同属性的元组类型--定义了长度,每个元素的类型是已知的--但具体的形状还没有被定义。

直接从拉动请求中的一个例子

type Foo<T extends unknown[]> = [string, ...T, number];

type T1 = Foo<[boolean]>;  // [string, boolean, number]
type T2 = Foo<[number, number]>;  // [string, number, number, number]
type T3 = Foo<[]>;  // [string, number]

我们在函数中已经有了类似的休息元素(后面会有更多介绍),但最大的区别是,变量元组类型可以发生在元组的任何地方,而且是多次。

type Bar<
  T extends unknown[],
  U extends unknown[]
> = [...T, string, ...U];

type T4 = Bar<[boolean], [number]>;  // [boolean, string, number]
type T5 = Bar<[number, number], [boolean]>;  // [number, number, string, boolean]
type T6 = Bar<[], []>;  // [string]

已经很不错了但是我们为什么要这么关心它呢?

函数参数是元组#

每个函数头都可以用一个元组类型来描述。比如说。

typescript declare function hello(name: string, msg: string): void;

是一样的。

typescript declare function hello(...args: [string, string]): void;

而且我们可以非常灵活地定义它。

declare function h(a: string, b: string, c: string): void
// equal to
declare function h(a: string, b: string, ...r: [string]): void
// equal to
declare function h(a: string, ...r: [string, string]): void
// equal to
declare function h(...r: [string, string, string]): void

这也被称为休息元素,这是我们在JavaScript中的东西,它允许你用一个几乎无限的参数列表来定义函数,其中最后一个元素,休息元素把所有多余的参数吸进去。

我们可以利用这一点,例如,这个通用元组函数接收任何类型的参数列表并从中创建一个元组。

function tuple<T extends any[]>(...args: T): T {
    return args;
}

const numbers: number[] = getArrayOfNumbers();
const t1 = tuple("foo", 1, true);  // [string, number, boolean]
const t2 = tuple("bar", ...numbers);  // [string, ...number[]]

问题是,休息元素总是要放在最后。在JavaScript中,不可能定义一个几乎无穷无尽的参数列表,只是在两者之间。

然而,通过变量元组类型,我们可以做到这一点例如,这是一个函数类型,开头的参数列表没有定义,但最后一个元素必须是一个函数。

type HasCallback<T extends unknown[]> =
  (...t: [...T, (...args: any[]) => any]) => void;

declare const foo: HasCallback<[string]>

foo('hello', function() {}) // 👍
foo('hello') // 💥 breaks

declare const bar: HasCallback<[string, number]>

bar('hello', 2, function() {}) // 👍
bar('hello', function() {}) // 💥 breaks
bar('hello', 2) // 💥 breaks

这就是现在的显式类型注解,但是和通用类型一样,我们也可以通过使用来推断它们 😎 这给我带来了一个有趣问题的解决方案。

Typing promisify#

在最后采取回调的函数在异步编程中很常见。在Node.js中,你经常会遇到这种模式。回调前的参数列表根据函数的目的不同而不同。

这里有几个虚构的例子。

// loads a file, you can set the encoding
// the callback gets the contents of the file
declare function load(
  file: string,
  encoding: string,
  callback: (result: string) => void): void

// Calls a user defined function based on 
// an event. The event can be one of 4 messages
type Messages = 'open' | 'write' | 'end' | 'error'
declare function on(
  msg: Messages,
  callback: (msg: { type: Messages, content: string}) => void
): void

当你进行异步编程时,你可能想使用承诺。有一个很好的函数来承诺基于回调的函数。它们与基于回调的函数接受相同的参数列表,但不是接受一个回调,而是返回一个带有结果的Promise。

我们可以使用变量元组类型来打字。

首先,我们设计一个类型,它可以推断出除最后一个参数以外的所有参数。

type InferArguments<T> =
  T extends (... t: [...infer Arg, (...args: any) => any]) => any ? 
    Arg : never

它的内容是:T是一个有休息元素的函数,其中元组包括

  • 我们推断出的任何变量元组Arg
  • 一个有任何参数的回调函数

我们返回Arg

我们还想从回调函数中推断出结果。类似的类型,稍作修改。

type InferCallbackResults<T> = 
  T extends (... t: [...infer Arg, (res: infer Res) => any]) => any ? 
    Res : never

promisify 函数接收任何符合参数+回调形状的函数。它返回一个除了回调之外具有相同参数列表的函数。然后这个函数返回一个带有回调结果的承诺。😅

declare function promisify<
  // Fun is the function we want to promisify
  Fun extends (...arg: any[]) => any 
>(f: Fun): 
  // we return a function with the same argument list
  // except the callback
  (...args: InferArguments<Fun>) 
    // this function in return returns a promise
    // with the same results as the callback
    => Promise<InferCallbackResults<Fun>>

这个声明已经很好了,函数体的实现检查没有类型转换,这意味着类型真的很健全。

function promisify<
  Fun extends (...args: any[]) => any
>(f: Fun): (...args: InferArguments<Fun>) => Promise<InferCallbackResults<Fun>> {
  return function(...args: InferArguments<Fun>) {
    return new Promise((resolve) => {
      function callback(result: InferCallbackResults<Fun>) {
        resolve(result)
      }
      args.push(callback);
      f.call(null, ...args)
    })
  }
}

在行动中。

const loadPromise = promisify(load)

loadPromise('./text.md', 'utf-8').then(res => {
  // res is string! 👍
})


const onPromise = promisify(on)

onPromise('open').then(res => {
  console.log(res.content) // content and type infered 👍
})

所有这些最棒的部分是我们保留了参数名称。当我们调用loadPromise ,我们仍然知道参数是fileencoding 。 ❤️