網站搜尋

帶有泛型錯誤的 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 子類型少,且沒有辨別標記)。