TypeScript 4.0应该会在2020年8月发布,而这个版本最大的变化之一就是变量元组类型。尽管在写这篇文章的时候,他的功能还很热,但还是值得一看,看看我们能用它做什么。请注意,这里的东西可能会有变化,所以要谨慎!在4.0进入RC或发布之前,我将努力保持这个页面的更新。
如果你想自己试试,你可以把早期版本的分支加载到TypeScript的操场上。
变体元组#
TypeScript中的元组类型是一个具有以下特征的数组。
- 数组的长度被定义。
- 每个元素的类型是已知的(而且不一定是相同的)。
例如,这是一个元组类型。
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 ,我们仍然知道参数是file 和encoding 。 ❤️