带有通用错误的打字稿缩小类型

时间:2021-05-11 08:07:25

标签: typescript generics

谁能帮我理解这里发生了什么:TS playground

基本上我有一个 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
}

更新: 我将参数移出并有 it working 但仍然不明白为什么嵌套它们不起作用

interface Store<Options> {
    exec: (nextState: Options) => void
}
// parent2: Store2<Param<ParentOptions>>,
// sub2: Store2<Param<SubOptions>>,

1 个答案:

答案 0 :(得分:3)

要回答您的问题,首先,让我们快速回顾一下不同“方差”的含义。在下表中,我使用了 Microsoft .NET documentation 中的定义(文档中没有的二元性除外),因为我发现它们最容易掌握:

<头>
差异 含义 允许替换
二元性 同时进行协方差和逆变 超类型 -> 子类型,子类型 -> 超类型
协方差 使您能够使用比最初指定的更派生的类型 超类型 -> 子类型
逆变 使您能够使用比最初指定的更少派生的类型 子类型 -> 超类型
不变性 表示只能使用原来指定的类型

让我们检查您的哪个类型是超类型,哪个是子类型:

type T1 = SubOptions extends ParentOptions ? true : false; // false
type T2 = ParentOptions extends SubOptions ? true : false; // true

因此 PartentOptionsSubOptions 的子类型,而后者是其超类型。它告诉我们什么?它告诉我们,当您将 subFlow 注释为 Store<SubOptions>,然后尝试为其分配 parentFlow(注释为 Store<ParentOptions>)时,您正在尝试分配一个 子类型需要超类型的地方

如果我们参考方差表,我们会看到这需要协方差,但是当你得到一个错误时,这意味着我们正在处理逆变或< em>不变性。现在,当您将 subFlow 分配给 parentFlow 时,您正在分配一个超类型,其中需要一个子类型

以上也导致了错误,这意味着这里的赋值实际上是不变的,而@captain-yossariancomment是正确的:

<块引用>

我相信这是因为 subFlow 和 parentFlow 彼此不变。

然而,这种行为是 TypeScript 的设计限制(请参阅 Anders Hejlsberg 的 comment 相关问题)牺牲了一些灵活性以保证稳健性(删除 [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

Playground


0 你的命名选择稍微增加了一个已经很棘手的问题的混乱:一个子类型被称为 ParentOptions 和超类型 SubOptions,而它们之间的关系是相反的,所以我相应地将它们命名为 SubOptionsSuperOptions 以使事情更清楚。

1 从评论中的讨论来看,必须指出,虽然解决方案中Store<Param<SubOptions>>Store<Param<SuperOptions>>之间的关系是协变T[keyof T] 这里是逆变(参见 Anders 的 comment - SuperOptions 超类型比 SubOptions 子类型具有更少的属性,并且没有判别式)。