在打字稿中链接函数类型



如果一个函数数组,其中一个函数的输出是下一个函数的输入,即输入和输出类型必须对齐每对,但可能因对而异。 如何启用打字稿编译器来理解键入:

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

法典

TypeScript 没有办法表达提供reduce()forEach()调用签名所需的高阶类型,这些调用签名允许您像这样安全地将函数链接在一起。 您也不能通过for循环来做到这一点。 除非你真的展开循环并调用arr[2](arr[1](arr[0]())),否则如果你犯了错误,你就无法实现这种事情,让编译器抓住你。 因此,实现将涉及一定数量的断言,您只需告诉编译器您正在做的事情是可以的。

话虽如此,如果你想要一个可重用的callChain()函数,它将一个"正确链接的函数"数组作为输入,你可以给该函数一个调用签名,它检查数组的"适当性"(专有性?)并返回正确的类型。 该实现会有些不安全,但您只需要编写一次该实现,并希望多次调用它。

这是一种可能的方法:

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: Extract<TupleToArgs<T>[I], any[]>) => 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());
}

在我介绍它是如何工作的之前,让我们使用您的示例来确保它有效:

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 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中是泛型的,一个元组类型对应于链中每个函数的返回类型。 所以如果T[string, number, boolean],这意味着链中的第一个函数返回一个string,第二个返回一个number,第三个返回一个boolean。 因此,TupleToChain<T>类型应将此类类型元组转换为正确类型的函数元组。callChain的返回类型是Last<T>,希望它应该只是T元组的最后一个元素。

该函数编写为单调用签名重载函数。 TypeScript 有意更松散地检查重载函数实现(有关原因,请参阅 ms/TS#13235),这样可以更轻松地将函数拆分为强类型调用端和弱类型实现。 该实现使用了很多any类型来回避编译器错误。 正如我在开始时所说,实现实际上无法正确进行类型检查,因此我正在使用any来阻止编译器尝试。

在运行时,我们将数组拆分为数组的第一个元素(不带输入)和数组的其余元素(它们都接受输入),然后使用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: Extract<TupleToArgs<T>[I], any[]>) => T[I] };

这只是一个映射的元组类型,它采用T中的裸类型并生成新的函数元组。 对于输入元组T中索引I处的每个类型,输出元组中的函数将返回类型T[I],并将一个称为TupleToArgs<T>[I]的东西作为参数列表。 因此,TupleToArgs<T>[I]最好给我们链中第I个函数所需的参数列表。 如果I0,那么我们想要一个像[]这样的空列表,而如果I对某些JJ+1,那么我们想要一个包含单元素[T[J]]的列表。 (无论如何,这将是一个数组类型,但编译器在这里看不到它,因为 TS4.7,所以我用Extract<..., any[]>包装它以说服编译器这一事实。

非常清楚的是,如果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()值得它的复杂性。 发生这种情况的确切位置是主观的;在我看来,你必须在你的代码库中经常做这些元组类型的函数链,它才值得。 但是,无论您是否实际使用它,考虑一下该语言可以使您在类型系统中表示此类操作的程度是很有趣的。

操场链接到代码

下面是一些代码,用于使用您提供的元组类型构建处理器,并使用类型检查执行某些操作。

就可扩展性而言,这比arr[2](arr[1](arr[0]()))更糟糕,请参阅jcalz的评论。

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>,
K extends (keyof T) & `${number}`
= (keyof T) & `${number}`,
U extends { [index in K & `${number}`]: T[index] & { (...args: any): any } }
= { [index in K]: (T[index] extends { (...args: any): any } ? T[index] : never) }
>(array: T) {
return function (step: (opt: {
[Index in keyof U]: {
index: Index,
value: ReturnType<U[Index]>
next: (opt: ReturnType<U[Index]>) => void
}
}[keyof U]) => void) {
let prevValue: ReturnType<U[keyof U]> | undefined = undefined
for (let index in array) {
if (index === '0') {
prevValue = array[index]()
}
else {
prevValue = array[index](prevValue)
}
let nextValue: ReturnType<U[keyof U]> | undefined = undefined
let nextCalled = false
step({
index: index as any,
value: prevValue as any,
next: function (v: any) {
nextValue = v
nextCalled = true
} as any
})
if (nextCalled === false) {
throw 'next() not called.'
}
prevValue = nextValue
}
return prevValue
}
}
let ret = builder(arrayOfFunctions)(function (opt) {
if (opt.index == '0') {
console.log(opt.index, opt.value)
opt.next(opt.value)
}
if (opt.index == '1') {
console.log(opt.index, opt.value)
opt.next(opt.value)
}
if (opt.index == '2') {
console.log(opt.index, opt.value)
opt.next(opt.value)
}
})
console.log(ret)

游乐场链接

arrayOfFunctions元组类型,我们可以与歧视性工会建立统一。

最新更新