TypeScript 不會將泛型參數擴散到匹配的函數中
我有一個包含函數的物件:
const bazApi = {
fun1: () => string,
fun2: (n: number) => void,
fun3: (s: string) => Promise<number>,
}
然後我有一個函數,它接受該物件的一個鍵和一個與關聯函數的輸入匹配的參數元組,即 fun2 和 [42]。該函數接著使用該標識符從物件中取得函數,並使用提供的參數呼叫它。然而,Typescript 抱怨說:
展開的參數必須具有元組類型,或者被傳遞給剩餘參數.ts(2556)
我已經查閱過這個錯誤,但我認為這不是根本問題,本質上是 TypeScript 突然無法匹配函數和參數。
這是程式碼(以及它在 Playground 的樣子):
type BazApiInterface = {
fun1: () => string,
fun2: (n: number) => void,
fun3: (s: string) => Promise<number>,
}
type Message<T extends Record<K, (...args: any) => any>, K extends keyof T> = {
command: K,
args: Parameters<T[K]>
}
type ApiListener<T extends Record<keyof T, (...args: any) => any>> = <K extends keyof T>(
message: Message<T, K>
) => void
declare const bazApi: BazApiInterface
const listener: ApiListener<BazApiInterface> = (message) => {
const { command, args } = message;
const handler = bazApi[command]; // TS says handler is function from BazApiInterface indexed by CommandName (good)
const res = handler(...args); // TS says handler is union over (in TS 4.7.4) or a merge of (?) all function from BazApiInterface (not good)
};
有趣的是,在錯誤上方的那一行,設置 handler 的地方,TypeScript 似乎知道它是由命令名稱識別的對象中的函數,即 BazApiInterface[K],我甚至可以讓它將其寫出來為
const handler: ApiInterface<{
fun1: () => string;
fun2: (n: number) => void;
fun3: (s: string) => Promise<number>;
}>[CommandName]
另外,args 會給出類似的輸出,TS 似乎知道它包含由 CommandName 指定的函數的參數。所以看起來 TS 應該能夠判斷它們是匹配的。
然而,當在下一行實際調用函數時,TS 會將 handler 視為對象中所有函數的聯合類型(在 TS v4.7.4 中)或某種奇怪的函數混合((arg0: never) => string | void | Promise<number>)在 TS v4.9.5 中,然後正確地抱怨某些地方不對。但如果函數類型沒有改變,情況不就正常了嗎?
目前,我只是作弊(const res=(handler as Function)(...args);),但我真的很想知道為什麼會發生這種情況,以及我是否可以對此做點什麼?
感覺這個問題幾乎每天都有人問,但我找不到解釋。這個答案以及 TS 的 GitHub 上的相關問題看起來很接近,但我覺得它不適用(至少我不能應用)。
答案#1
你遇到的問題是
const res = handler(...args);
問題在於編譯器無法追蹤 handler 的類型與 args 的類型之間的關聯。有關展開/剩餘參數的錯誤訊息具有誤導性,詳見 microsoft/TypeScript#47615。Parameters<T> 工具類型是以條件型別實作的,因此當其參數為泛型時,它是延遲解析的。雖然人類可以閱讀 func(...args),其中 func 為泛型類型 F,args 為泛型類型 Parameters<F>,並能說“這樣沒問題”,編譯器卻不是這麼看的,因為它並不真正知道 Parameters 的用途。只有當編譯器知道 args 的類型是 A,且 func 的類型是 (...args: A) => any 時,才能泛型地呼叫 func(...args)。
如果你想嘗試重構,以便編譯器確實遵循這種相關性,你確實可以使用 microsoft/TypeScript#47109 中提到的技術,在那裡我們嘗試以對映射類型的泛型索引訪問的方式來表示事物。
例如:
declare const bazApi: BazApiInterface
type Api<T extends Record<keyof T, (...args: any) => any>> =
{ [K in keyof T]: (...args: Parameters<T[K]>) => ReturnType<T[K]> }
const listener: ApiListener<BazApiInterface> =
(message) => {
const _bazApi: Api<BazApiInterface> = bazApi;
const { command, args } = message;
const handler = _bazApi[command];
const res = handler(...args); // okay
};
Api<T> 類型本質上是一個對 T 的恒等函數,或多或少,實際上 Api<BazInterface> 等同於 BazInterface。 編譯器也會允許你將 bazApi 賦值給 _bazApi。 但是由於 Api<T> 是一個映射類型,那麼 _bazApi 的類型也同樣是映射類型。 因此 _bazApi[command] 現在是一個對映射類型的索引訪問,編譯器可以直接將其評估為類型 (...args: Parameters<BazInterface[K]>) => ReturnType<BazInterface[K]>。 並且由於 args 的類型是 Parameters<BazInterface[K]>,因此調用成功。
再說一次,將 BazInterface 重寫為等效類型後事情可以運作,這看起來可能很愚蠢,但你可以將其視為引導編譯器理解 handler 和 args 之間泛型關係的練習。
程式碼的 Playground 連結
