以下是我如何写出buildSelector()
:
function buildSelector<T extends any[], U>(...args: [
...selectors: { [I in keyof T]: (state: State) => T[I] },
callback: (...args: T) => U
]): (state: State) => U {
const selectors = args.slice(0, -1) as ((state: State) => any)[];
const callback = args.at(-1) as (...args: any) => U;
return (state) => callback(...selectors.map(f => f(state)))
}
调用签名相当复杂,主要是因为我们需要表示我们希望在单个回调函数中接收任意数量的 Select 器followed by.这就好比使用一个rest parameter,然后在后面加上一个额外的参数.但JavaScript只支持将REST参数放在参数列表的第end位.因此,从概念上讲,该函数如下所示
// not valid TS nor valid JS, do not use:
function buildSelector<T extends any[], U>(
...selectors: { [I in keyof T]: (state: State) => T[I] },
callback: (...args: T) => U
): (state: State) => U {
return (state) => callback(...selectors.map(f => f(state)))
}
让我们先看一下假设的版本,然后再讨论现实世界的版本.该函数是T
中的generic,这是callback
的参数的tuple type,以及selectors
数组的return types的元组.因此,selectors
的类型是mapped tuple type除以T
,因此selectors
的第I
个元素是类型(state: State) => T[I]
,这是一个接受State
的函数,它返回T
的第I
个元素.在返回类型callback
的U
中,buildSelector
也是泛型的.
假设的实现也相当简单:它是一个接受state
并在selectors
的map
pped版本上调用callback
的函数,其中selectors
的每个元素都以state
作为参数进行调用.请注意,当对元组使用map()
时,编译器不可能遵循所涉及的逻辑,请参见Mapping tuple-typed value to different tuple-typed value without casts.因此,在现实版本中,实现将需要大约type assertions个元素,在我们的假设世界中,这并不是必需的,因为在我们的假设世界中,领先的REST元素在JavaScript中是被接受的.
好了,现在回到现实世界:
function buildSelector<T extends any[], U>(...args: [
...selectors: { [I in keyof T]: (state: State) => T[I] },
callback: (...args: T) => U
]): (state: State) => U {
const selectors = args.slice(0, -1) as ((state: State) => any)[];
const callback = args.at(-1) as (...args: any) => U;
return (state) => callback(...selectors.map(f => f(state)))
}
由于参数列表可以是可变长度的,变量部分在开头,我们别无 Select ,只能在开头给函数一个args
个REST参数.虽然在JavaScript中不支持前导REST参数,但幸运的是,有is个支持leading rest tuple element英寸打字的脚本.因此,args
的类型是一个可变元组,具有前导的REST selectors
元素和最后的callback
元素.它们的类型与以前相同.
在函数实现内部,我们需要将args
分解为selectors
和callback
.我们不能用像const [...selectors, callback] = args
这样的destructuring assignment来做到这一点,因为在那里,JavaScript也不支持领先的REST元素.所以我们使用像slice()
和at()
这样的数组方法.当然,你可以随心所欲地go 做.
我使用断言和the any
type来赋予selectors
和callback
足够宽松的类型,这样编译器就不会抱怨return (state) => callback(...selectors.map(f => f(state)))
.如前所述,我们需要它们,因为编译器无法理解selectors.map(f => f(state))
将返回类型T
的值.但我们也需要它们,因为编译器无法理解at(-1)
将生成最终的回调函数类型(而不是 Select 器类型),而slice(0, -1)
将生成 Select 器类型的元组(而不是最终的回调类型).也就是说,分割和拆分元组类型的数组不会产生元组类型.有一个功能请求支持负指数的强类型,at
在microsoft/TypeScript#47660,但被拒绝了,因为太复杂了.因此,您在这里所能做的最好的事情就是断言实现是正确的,并放弃对编译器的大量验证.
好吧,这就是我的解释.让我们看看它是否奏效:
const selector = buildSelector(
selectName, selectAge, (name, age) => `${name} - ${age}`
);
const x = selector(state);
// const x: string
console.log(x) // "John Doe - 38"
看上go 不错!编译器contextually infers将最终回调的name
和age
参数类型分别为string
和number
.它还理解selector
是(state: State)=>string
类型,因为回调返回类型是string
.您可以看到,该实现也能按预期工作.
Playground link to code个