TypeScript泛型中的类型缩小错误

3

有人可以帮我理解这里发生了什么吗:TS 播放器

基本上我有一个具有 exec 方法的 store,我想要缩小子进程中 exec 参数的类型。但是似乎存储类型是一种通用类型,存在错误。

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 移出来并且已经it working,但仍然不明白为什么它们的嵌套不起作用。
interface Store<Options> {
    exec: (nextState: Options) => void
}
// parent2: Store2<Param<ParentOptions>>,
// sub2: Store2<Param<SubOptions>>,

2
我认为这是因为subFlowparentFlow彼此不变。请参见我的问题:https://dev59.com/OVEG5IYBdhLWcg3wac6f - captain-yossarian from Ukraine
1
在Scala中,您可以决定泛型参数是协变、逆变还是不变。关于不变性有一个很好的语法:class Foo[-A]。 - captain-yossarian from Ukraine
1
@captain-yossarian,我认为有一个待定的提案,用于为泛型参数添加方差标记。如果没有人比我更快地找到它,我会在找到后将其链接给你。 - Oleg Valter is with Ukraine
1
很遗憾,他们的路线图上没有这个计划。 - captain-yossarian from Ukraine
1
@captain-yossarian - 是的,没错,只是想为读者和更多曝光再次提出来(Ryan提到他们仍在研究中)。我不知道显式方差注释是否是一个好的选择,但它们肯定可以很好地解决OP的问题。 - Oleg Valter is with Ukraine
显示剩余5条评论
1个回答

3

回答你的问题前,让我们快速回顾一下"方差"有哪些不同的含义。在下表中,我使用了来自微软.NET文档(除了不在文档中的双变性)的定义,因为我觉得它们最容易理解:

差异 含义 允许替换
双变性 同时具有协变和逆变性 超类型 -> 子类型,子类型 -> 超类型
协变性 允许使用比最初指定的更派生的类型 超类型 -> 子类型
逆变性 允许使用比最初指定的更不派生的类型 子类型 -> 超类型
不变性 表示只能使用最初指定的类型

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

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

这意味着ParentOptionsSubOptions的一个子类型,而后者是其超类型。这告诉我们什么?这告诉我们当你将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,但它们之间的关系是相反的,所以我将它们命名为SubOptionsSuperOptions,以使事情更清晰。

1从评论中的讨论可以看出,在解决方案中Store<Param<SubOptions>>Store<Param<SuperOptions>>之间的关系是协变的,但这里的T[keyof T]逆变的(请参见Anders的评论 - SuperOptions超类型比SubOptions子类型少一些属性,并且没有区分因素)。


1
我发现你的表格和解释非常有用。我希望我能向你捐赠100点声望 :) 感谢所有链接。很难找到关于方差/协方差等方面的好文章。超级棒的回答! - captain-yossarian from Ukraine
1
@captain-yossarian - 一旦我确信这种关系实际上是不变的,就很容易找到了 :) 但是这需要一段时间才能达到。顺便说一句,感谢您在上面链接柏林聚会视频 - 我通常不看YT讲话,但Titian关于方差的演讲非常棒。 - Oleg Valter is with Ukraine
1
这是我的理解展示:https://tsplay.dev/wRpqLm - Tubc
@Tubc - 你评论时我不在,让我先看看这个 playground,一旦我理解了那里发生了什么,我会尽快回复你。从浏览它的方式来看,我认为我们没有分歧,但是我应该更好地措辞关于别名的部分并进行扩展。Param<T>确实是逆变的,Store<T>也是如此。然而,在“参数类型变成协变”中,我指的是Store<Param<SuperOptions>>Store<Param<SubOptions>>之间的关系,这在您和我的 playground 中都被证实是协变的。 - Oleg Valter is with Ukraine
@Tubc - 至于Anders提出的另一个评论,是的,我进行了分析,但决定不包含它,以避免造成更多的混乱。你的情况肯定属于第二种情况,即超类型比子类型具有更少的属性,这意味着在此处T[keyof T]是逆变的。 - Oleg Valter is with Ukraine
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接