[英]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.