帶有泛型錯誤的 Typescript 縮窄類型
有人可以幫我理解這裡發生了什麼嗎:TS 遊樂場
基本上我有一個 store,它有一個 exec 方法,我想縮小子流程的 exec 參數類型,但好像 store 類型作為泛型類型時會出現錯誤
type Param<Options> = {
[K in keyof Options]: Readonly<{
id: K,
options: Options[K],
}>
}[keyof Options];
interface Store<Options> {
exec: (nextState: Param<Options>) => void
}
type ParentOptions = {
'a': { a: string },
} & SubOptions
type SubOptions = {
'b': { b: number },
}
function test(
parentFlowExec: (nextState: Param<ParentOptions>) => void,
subFlowExec: (nextState: Param<SubOptions>) => void,
parentNonGeneric: { exec: (nextState: Param<ParentOptions>) => void },
subNonGeneric: { exec: (nextState: Param<SubOptions>) => void },
parentFlow: Store<ParentOptions>,
subFlow: Store<SubOptions>,
) {
parentFlowExec = subFlowExec; // error: ok
subFlowExec = parentFlowExec; // passed
parentNonGeneric = subNonGeneric; // error: ok
subNonGeneric = parentNonGeneric; // passed
parentFlow = subFlow; // error: ok
subFlow = parentFlow; // error ??
// I plan to use it like this
subProcess(parentFlow);
}
function subProcess(flowStore: Store<SubOptions>) {
flowStore.exec({ id: 'a', options: { a: 'a' } }); // can't call with 'a'
flowStore.exec({ id: 'b', options: { b: 3 } }); // ok
}
更新:我把 Param 移出來了,現在可以運作,但仍然不明白為什麼巢狀放置時會無法運作
interface Store<Options> {
exec: (nextState: Options) => void
}
// parent2: Store2<Param<ParentOptions>>,
// sub2: Store2<Param<SubOptions>>,
答案#1
為了回答你的問題,首先,讓我們快速回顧一下不同「變異」的意思。在下表中,我使用了微軟 .NET 文件中的定義(除了文件中沒有提到的雙變異),因為我覺得它們最容易理解:
| 變異 | 意思 | 允許的替代 |
|---|---|---|
| 雙變量 | 同時的協變與反變 | 超類別 -> 子類別, 子類別 -> 超類別 |
| 協方差 | 允許您使用比最初指定的類型更衍生的類型 | 超類別 -> 子類別 |
| 反變 | 允許您使用比最初指定的類型衍生程度更低的類型 | 子類型 -> 超類型 |
| 不變性 | 這意味著你只能使用最初指定的類型 | 無 |
讓我們來檢查你的哪一種類型是超類型,哪一種類型是子類型:
type T1 = SubOptions extends ParentOptions ? true : false; // false
type T2 = ParentOptions extends SubOptions ? true : false; // true
由此可見,ParentOptions 是 SubOptions 的子類型,而後者是其超類型。這告訴我們什麼?它告訴我們,當你將 subFlow 註解為 Store<SubOptions>,然後嘗試將 parentFlow(註解為 Store<ParentOptions>)賦值給它時,你正在嘗試 在期望超類型的地方賦予子類型。
如果我們參考變異表,我們會看到這需要協變,但由於你收到錯誤,這意味著我們正在處理逆變或不變。現在,當你將subFlow指派給parentFlow時,你是在將在預期子類型的位置使用超類型。
上述也會導致錯誤,這意味著此處的賦值實際上是不變的,而@captain-yossarian的評論是正確的:
我相信這是因為 subFlow 和 parentFlow 對彼此是不變的。
然而,這種行為是 TypeScript 的設計限制(參見 Anders Hejlsberg 在相關問題上的評論),為了類型安全犧牲了一些靈活性(移除 [keyof Options] 索引,你將會看到逆變賦值變得可能)。
至於你的解決方案,由於變異分析的運作方式,當你將 Params 向外移動時,參數類型會變為 協變(因為這裡的 T[keyof T] 並未被重命名。注意,當簡化到最基本結構時,Param 類型正是如此:type Param<Options>=Options[keyof Options],僅經映射1)。
看看你解決方案的一個簡化例子0:
type Param<Options> = {
[K in keyof Options]: Readonly<{
id: K,
options: Options[K],
}>
}[keyof Options];
interface Store<Options> {
exec: (nextState: Options) => void
}
type SuperOptions = { 'b': { b: number } }
type SubOptions = { 'a': { a: string } } & SuperOptions
const test1 = (subtype: Store<Param<SubOptions>>) => subProcess1(subtype); // OK, Subtype -> Supertype, covariance
const test2 = (supertype: Store<Param<SuperOptions>>) => subProcess2(supertype); // error, Supertype -> Subtype, contravariance
const subProcess1 = (supertype: Store<Param<SuperOptions>>) => supertype.exec({ id: 'b', options: { b: 3 } }); // ok
const subProcess2 = (subtype: Store<Param<SubOptions>>) => subtype.exec({ id: 'b', options: { b: 3 } }); // ok
遊樂場
0 你的命名選擇讓已經很棘手的問題更混亂:子類型被稱為 ParentOptions,而超類型被稱為 SubOptions,然而它們之間的關係恰好相反,所以我相應地將它們命名為 SubOptions 和 SuperOptions 以使事情更清楚。
1 從評論中的討論來看,必須注意,雖然解決方案中 Store<Param<SubOptions>> 和 Store<Param<SuperOptions>> 之間的關係是 協變,但這裡的 T[keyof T] 是 逆變(參見 Anders 的評論——SuperOptions 超類型的屬性比 SubOptions 子類型少,且沒有辨別標記)。
