在使用帶有泛型函數類型的 Parameters 時,如何在 TypeScript 中強制類型安全?
我正在嘗試在使用 Parameters 工具型別時,在 TypeScript 中實現型別安全。然而,我遇到了一個問題,即 Parameters 型別在推斷參數型別時,並未考慮泛型函數型別 TFunc。
這裡是一個涉及的函數範例:
const funcExecutor = <TFunc extends (...args: any[]) => void>(data: {
func: TFunc;
mapParams: () => Parameters<TFunc>[0];
}) => data.func(data.mapParams());
const funcExecutorHandler = <TAction extends (arg: any) => any>(
action: TAction,
data: Parameters<TAction>[0],
) => {};
funcExecutor 函數是類型安全的,可以如下調用:
funcExecutor({
func: (param: { foo: boolean }) => param,
mapParams() {
return {
foo: true, // TYPE SAFE
};
},
});
當使用 funcExecutorHandler 函數時會出現問題,這是一個通用函數,可以接受任何函數,並根據提供函數的參數類型期望相應的參數。在這個例子中,目的是強制對 mapParams 函數的類型安全。然而,它未能達到這個目的。
funcExecutorHandler(funcExecutor, {
func: (param: { foo: boolean }) => param,
mapParams() {
return {
foo: 'bad', // NOT TYPE SAFE
};
},
});
問題在於 Parameters<TAction>[0] 類型未考慮泛型函數類型 TFunc,導致無法對 mapParams 函數強制執行類型安全。
在這種情況下,有沒有辦法讓 Parameters 的類型推斷依賴於泛型函數類型 TFunc?任何建議或替代方案都將不勝感激。謝謝!
答案#1
TypeScript 本質上無法執行您所尋求的那種高階類型操作。推斷演算法無法執行任意的泛型類型實例化,而這正是您需要讓這個工作運作的。更多資訊請參見 microsoft/TypeScript#52010 和 microsoft/TypeScript#49690。
所以 Parameters<T> 工具類型無法生成你需要的那種泛型類型。更多資訊請參見 microsoft/TypeScript#43731。
最終你會看到的是,「內部」泛型類型參數將被它們的約束所取代,這通常是 unknown。
此外,Parameters<T> 是以條件類型實現的,當 T 是泛型時,例如在 funcExecutor() 的函數體內,類型檢查器無法對這類型進行準確的分析。它有時會對安全的項目提出警告,因為它將類型保持為不透明,無法判斷什麼可能是可賦值的。其他時候,它會試圖通過過早評估類型來推進,從而允許不安全的操作。例如,你的 funcExecutor 函數實現是不安全的,但編譯器卻沒有檢測到它:
funcExecutor({
func: (x: string, y: number) => console.log(y.toFixed()),
mapParams: () => ""
}); // 💥 y is undefined
該呼叫符合 funcExecutor() 的呼叫簽名,其中 TFunc 的類型為 (x: string, y: number) => void,但你會遇到運行時錯誤,因為只傳遞了第一個參數。funcExecutor() 的函數體實際上應該會有編譯錯誤,但編譯器錯過了這個問題,因為它對 Parameters<TFunc> 的樣子做了錯誤的假設。
我在這裡的建議是將條件型別重構掉(你的函數實際上只需要對單一參數型別使用泛型,而不是整個函數型別),並使用實例化表達式來指定編譯器無法推斷的型別參數。像這樣:
const funcExecutor = <A,>(data: {
func: (arg: A) => void;
mapParams: () => A;
}) => data.func(data.mapParams());
const funcExecutorHandler = <A,>(
action: (arg: A) => void,
data: A,
) => { };
funcExecutor({
func: (param: { foo: boolean }) => param,
mapParams() {
return {
foo: true, // TYPE SAFE
};
},
});
funcExecutor({
func: (x: string, y: number) => console.log(y.toFixed()), // error!
mapParams: () => ""
})
funcExecutorHandler(funcExecutor<{ foo: boolean }>, {
// -----------------------> ^^^^^^^^^^^^^^^^^^
func: (param: { foo: boolean }) => param,
mapParams() { // error!
return {
foo: 'bad',
};
},
});
現在你會在預期的地方收到錯誤:對 funcExecutor() 的不安全呼叫現在會成為編譯器錯誤,因為它需要比呼叫簽名允許更多的參數;而 funcExecutorHandler() 的呼叫會失敗,因為它知道 {foo: "bad"} 不是 {foo: boolean}。
這比你想要的類型指定稍多一點,但並不那麼糟,特別是因為它讓你不用標註func回呼參數的類型,所以在複雜度上差不多相等:
funcExecutorHandler(funcExecutor<{ foo: boolean }>, {
func: (param) => param, // known to be {foo: boolean}
mapParams: () => ({ foo: true })
});
程式碼的 Playground 連結
