[英]Chaining function types in typescript
如果一个函数具有一组函数,其中一个函数的 output 是下一个 function 的输入,即输入和输出类型必须为每对对齐,但在不同对之间可能不同。 如何启用 Typescript 编译器来理解打字:
type A = () => string
type B = (str: string)=> number
type C = (num: number)=> [number,number]
const a:A = ()=>'1'
const b:B = (str)=>parseInt(str)
const c:C = (num)=>[num,num]
//typescript is fine with this
console.log(`result: ${c(b(a()))}`)
//but not what follows, even though it's similar functionality
//this is not dynamic typing as the order in which functions
//are dealt with is known and fixed at compile time
const arrayOfFunctions: [A,B,C] = [a,b,c]
let prevResult: any
arrayOfFunctions.forEach(fn=>{
// nasty hack that breaks typing by casting everything to any
const hackedFn : (res?:any)=>any = fn as (res?:any)=>any
if(prevResult) prevResult = hackedFn(prevResult)
else prevResult = hackedFn()
})
console.log(`prevResult: ${prevResult}`)
如何在不中断输入的情况下执行arrayOfFunctions
?
这是一些代码,用于使用您提供的元组类型构建处理器并进行类型检查。
type A = () => string
type B = (str: string) => number
type C = (num: number) => [number, number]
const a: A = () => '1'
const b: B = (str) => parseInt(str)
const c: C = (num) => [num, num]
const arrayOfFunctions: [A, B, C] = [a, b, c]
function builder<T extends Array<any>>(array: T) {
return function (step: (opt: {
[Index in keyof T]: {
index: Index,
value: T[Index]
next: (opt: T[Index]) => void
}
}[number]) => void) {
let prevValue: T[number] | undefined = undefined
for (let ind = 0; ind < array.length; ind++) {
if (ind === 0) {
prevValue = array[ind]()
}
else {
prevValue = array[ind](prevValue)
}
let nextValue: T[number] | undefined = undefined
let nextCalled = false
step({
index: ind,
value: prevValue,
next: function (v) {
nextValue = v
nextCalled = true
}
})
if (nextCalled === false) {
throw 'next() not called.'
}
prevValue = nextValue
}
return prevValue
}
}
let ret = builder(arrayOfFunctions)(function (opt) {
if (opt.index == '0') { // Don't use ===
console.log(opt.index, opt.value)
opt.next(opt.value)
}
if (opt.index == '1') { // Don't use ===
console.log(opt.index, opt.value)
opt.next(opt.value)
}
if (opt.index == '2') { // Don't use ===
console.log(opt.index, opt.value)
opt.next(opt.value)
}
})
console.log(ret)
TypeScript 没有办法表达提供reduce()
或forEach()
调用签名所需的高阶类型,这些调用签名允许您像这样安全地将函数链接在一起。 你也不能用for
循环来做到这一点。 除非您实际上展开循环并调用arr[2](arr[1](arr[0]()))
,否则如果您犯了错误,您将无法实现这种事情,让编译器抓住您。 因此,实现将涉及一些断言,您只需告诉编译器您正在做的事情是可以的。
话虽如此,如果您想要一个可重用的callChain()
function 以“正确链接的函数”数组作为输入,您可以为 function 提供一个调用签名,以检查数组的“正确性”(适当性?)和它返回正确的类型。 该实现会有些不安全,但您只需要编写一次该实现,并希望多次调用它。
这是一种可能的方法:
type TupleToArgs<T extends any[]> =
Extract<[[], ...{ [I in keyof T]: [arg: T[I]] }], Record<keyof T, any>>;
type TupleToChain<T extends any[]> =
{ [I in keyof T]: (...args: TupleToArgs<T>[I]) => T[I] };
type Last<T extends any[]> =
T extends [...infer _, infer L] ? L : never;
function callChain<T extends any[]>(fns: [...TupleToChain<T>]): Last<T>
function callChain(funcs: ((...args: any) => any)[]) {
const [f0, ...fs] = funcs;
return fs.reduce((a, f) => f(a), f0());
}
在我 go了解它的工作原理之前,让我们使用您的示例确保它可以正常工作:
const result = callChain(arrayOfFunctions)
// const result: [number, number]
console.log(result) // [1, 1]
callChain([a, b, b]) // error!
// ------------> ~
// Type 'B' is not assignable to type '(arg: number) => number'.
// Types of parameters 'str' and 'arg' are incompatible.
// Type 'number' is not assignable to type 'string'.
那正是你想要的。 接受正确的链,返回类型为[number, number]
。 不正确的链会导致编译器错误,其中链中的第一个坏元素被突出显示为采用错误的输入类型。
所以,这很好。
所以,让我们解释一下它是如何工作的。 一、function本身:
function callChain<T extends any[]>(fns: [...TupleToChain<T>]): Last<T>
function callChain(funcs: ((...args: any) => any)[]) {
const [f0, ...fs] = funcs;
return fs.reduce((a, f) => f(a), f0());
}
调用签名在T
中是通用的,它是一个元组类型,对应于链中每个 function 的返回类型。 所以如果T
是[string, number, boolean]
,这意味着链中的第一个 function 返回一个string
,第二个返回一个number
,第三个返回一个boolean
。 因此, TupleToChain<T>
类型应该将这样的类型元组转换为正确类型的函数元组。 callChain
的返回类型是Last<T>
,它应该是T
元组的最后一个元素。
function 被编写为单呼号重载 function 。 Overloaded function implementations are intentionally checked more loosely by TypeScript, (see ms/TS#13235 for why this is), which makes it easier to split apart the function into the strongly-typed call side and the weakly-typed implementation. 该实现使用了很多any
类型来回避编译器错误。 正如我在一开始所说的那样,实现不能真正进行正确的类型检查,所以我使用any
来阻止编译器甚至尝试。
在运行时,我们将数组拆分为第一个元素(不接受输入)和数组的 rest(都接受输入),然后使用reduce()
数组方法进行实际链接。
现在让我们看看类型。 最简单的是Last
:
type Last<T extends any[]> =
T extends [...infer _, infer L] ? L : never;
那只是使用可变元组类型和条件类型推断来从元组中提取最后一个元素。
现在对于TupleToChain
:
type TupleToChain<T extends any[]> =
{ [I in keyof T]: (...args: TupleToArgs<T>[I]) => T[I] };
这只是一个映射元组类型,它采用T
中的裸类型并生成一个新的函数元组。 对于输入元组T
中索引I
处的每种类型, output 元组中的 function 将返回类型T[I]
,它将作为TupleToArgs<T>[I]
的列表所以TupleToArgs<T>[I]
最好给我们链中第I
个 function 所需的 arguments 列表。 如果I
是0
,那么我们想要一个类似[]
的空列表,而如果I
是某个J
的J+1
,那么我们想要一个包含单个元素[T[J]]
的列表。
要痛苦地清楚,如果T
是[string, number, boolean]
,我们希望TupleToArgs<T>[0]
是[]
以便形成()=>string
; 我们希望TupleToArgs<T>[1]
是[string]
以便形成(arg: string)=>number
; 我们希望TupleToArgs<T>[2]
是[number]
以便形成(arg: number)=>boolean
。
所以这里是TupleToArgs
:
type TupleToArgs<T extends any[]> =
Extract<[[], ...{ [I in keyof T]: [arg: T[I]] }], Record<keyof T, any>>;
这是另一种映射元组类型,我们将T
的每个元素包装在一个元组中(因此[string, number, boolean]
变为[[string],[number],[boolean]]
),然后我们在前面添加一个空元组它(所以我们得到[[],[string],[number],[boolean]]
)。 这确实完全符合我们希望TupleToArgs
的工作方式(最后的额外[boolean]
不会伤害任何东西)......
...但是我们使用Extract<T, U>
实用程序类型将它包装在一个额外的位Extract<..., Record<keyof T, any>>
中。 这实际上并没有改变类型,因为返回的元组确实可以分配给Record<keyof T, any>
。 但它有助于编译器看到对于I in keyof T
,类型TupleToArgs<T>[I]
是有效的。 否则编译器会感到困惑并担心I
可能不是TupleToArgs<T>
的关键。
基本上就是这样。 这有点令人费解,但不是我写过的最糟糕的东西。 我确信在某些极端情况下它不起作用。 当然,如果您的函数数组未定义为元组类型,您将度过一段糟糕的时光。 对于像[a, b, c]
这样的单个链,没有什么比c(b(a()))
更好的了。 但是如果你发现自己需要多次编写这样的链,那么像callChain()
这样的东西就值得它的复杂性了。 发生这种情况的确切位置是主观的; 在我看来,你必须经常在你的代码库中使用这些元组类型的 function 链,这样才值得。 但是无论您是否实际使用它,有趣的是考虑该语言可以使您在类型系统中表示此类操作有多接近。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.