【问题标题】:Discriminated Union of Generic type泛型的可区分联合
【发布时间】:2018-11-24 23:52:45
【问题描述】:

我希望能够将union discrimination 与泛型一起使用。但是,它似乎不起作用:

示例代码 (view on typescript playground):

interface Foo{
    type: 'foo';
    fooProp: string
}

interface Bar{
    type: 'bar'
    barProp: number
}

interface GenericThing<T> {
    item: T;
}


let func = (genericThing: GenericThing<Foo | Bar>) => {
    if (genericThing.item.type === 'foo') {

        genericThing.item.fooProp; // this works, but type of genericThing is still GenericThing<Foo | Bar>

        let fooThing = genericThing;
        fooThing.item.fooProp; //error!
    }
}

我希望 typescript 能够识别出,因为我区分了通用的 item 属性,所以 genericThing 必须是 GenericThing&lt;Foo&gt;

我猜这只是不支持?

另外,有点奇怪的是,在直接分配之后,它 fooThing.item 失去了它的歧视。

【问题讨论】:

  • 最后一行出现什么错误?仅从 genericThing 中提取项目,无论是在函数顶部还是通过在参数中解构,有什么不同吗?
  • @jonrsharpe 打开 typescript playground 链接,您可以看到它。 fooProp does not exist on type ...

标签: typescript discriminated-union


【解决方案1】:

问题

可区分联合中的类型缩小受到几个限制:

不展开泛型

首先,如果类型是泛型,泛型将不会被展开来缩小类型:缩小需要联合才能工作。因此,例如这不起作用:

let func = (genericThing:  GenericThing<'foo' | 'bar'>) => {
    switch (genericThing.item) {
        case 'foo':
            genericThing; // still GenericThing<'foo' | 'bar'>
            break;
        case 'bar':
            genericThing; // still GenericThing<'foo' | 'bar'>
            break;
    }
}

虽然这样做:

let func = (genericThing: GenericThing<'foo'> | GenericThing<'bar'>) => {
    switch (genericThing.item) {
        case 'foo':
            genericThing; // now GenericThing<'foo'> !
            break;
        case 'bar':
            genericThing; // now  GenericThing<'bar'> !
            break;
    }
}

我怀疑展开具有联合类型参数的泛型类型会导致编译器团队无法以令人满意的方式解决各种奇怪的极端情况。

嵌套属性不会缩小范围

即使我们有一个类型的联合,如果我们在嵌套属性上进行测试,也不会发生缩小。一个字段类型可能会根据测试缩小,但根对象不会缩小:

let func = (genericThing: GenericThing<{ type: 'foo' }> | GenericThing<{ type: 'bar' }>) => {
    switch (genericThing.item.type) {
        case 'foo':
            genericThing; // still GenericThing<{ type: 'foo' }> | GenericThing<{ type: 'bar' }>)
            genericThing.item // but this is { type: 'foo' } !
            break;
        case 'bar':
            genericThing;  // still GenericThing<{ type: 'foo' }> | GenericThing<{ type: 'bar' }>)
            genericThing.item // but this is { type: 'bar' } !
            break;
    }
}

解决方案

解决方案是使用自定义类型保护。我们可以制作一个非常通用的类型保护版本,它适用于任何具有type 字段的类型参数。不幸的是,我们无法为任何泛型类型创建它,因为它将绑定到GenericThing

function isOfType<T extends { type: any }, TValue extends string>(
  genericThing: GenericThing<T>,
  type: TValue
): genericThing is GenericThing<Extract<T, { type: TValue }>> {
  return genericThing.item.type === type;
}

let func = (genericThing: GenericThing<Foo | Bar>) => {
  if (isOfType(genericThing, "foo")) {
    genericThing.item.fooProp;

    let fooThing = genericThing;
    fooThing.item.fooProp;
  }
};

【讨论】:

  • 天哪……这个解决方案很粗糙!很高兴知道这是可能的。我的解决方案只是针对嵌套对象进行编码,而不是尝试接受缩小的泛型。 EG:让我的函数接受Foo(而不是直接接受GenericThing&lt;Foo&gt;,如果我需要做一些有区别的事情
  • @NSjonas 这也是一个选项 :-)
【解决方案2】:

正如@Titian 解释的那样,当您真正需要的是:

GenericThing<'foo'> | GenericThing<'bar'>

但你有一些定义为:

GenericThing<'foo' | 'bar'>

显然,如果您只有两个这样的选择,您可以自己扩展它,但这当然是不可扩展的。

假设我有一个带有节点的递归树。这是一个简化:

// different types of nodes
export type NodeType = 'section' | 'page' | 'text' | 'image' | ....;

// node with children
export type OutlineNode<T extends NodeType> = AllowedOutlineNodeTypes> = 
{
    type: T,
    data: NodeDataType[T],   // definition not shown

    children: OutlineNode<NodeType>[]
}

OutlineNode&lt;...&gt; 所代表的类型需要是discriminated union,这是因为type: T 属性。

假设我们有一个节点的实例,我们遍历子节点:

const node: OutlineNode<'page'> = ....;
node.children.forEach(child => 
{
   // check a property that is unique for each possible child type
   if (child.type == 'section') 
   {
       // we want child.data to be NodeDataType['section']
       // it isn't!
   }
})

显然,在这种情况下,我不想定义具有所有可能节点类型的子节点。

另一种方法是“分解”我们定义子代的NodeType。不幸的是,由于无法提取类型名称,因此我找不到使其通用的方法。

相反,您可以执行以下操作:

// create type to take 'section' | 'image' and return OutlineNode<'section'> | OutlineNode<'image'>
type ConvertToUnion<T> = T[keyof T];
type OutlineNodeTypeUnion<T extends NodeType> = ConvertToUnion<{ [key in T]: OutlineNode<key> }>;

那么children的定义就变成了:

 children: OutlineNodeTypeUnion<NodeType>[]

现在,当您遍历子代时,它是对所有可能性的扩展定义,并且被定义的联合类型保护会自行发挥作用。

为什么不直接使用 typeguard? 1)你真的不需要。 2)如果使用像角度模板之类的东西,你不希望你的打字机有无数次调用。这种方式实际上允许在*ngIf 内自动缩小类型,但不幸的是ngSwitch 内不允许

【讨论】:

  • 定义一种联合类型正是我一直在寻找的。谢谢!
【解决方案3】:

if 块内将表达式genericThing.item 视为Foo 是一个很好的观点。我认为它只有在将其提取到变量(const item = genericThing.item)后才有效。可能是最新版本 TS 的更好行为。

这启用了模式匹配,就像在Discriminated Unions 的官方文档中的函数area 中一样,而这实际上在 C# 中缺失(在 v7 中,default 大小写在像这样的 switch 语句中仍然是必需的)。

确实,奇怪的是genericThing 仍然被视为一视同仁(作为GenericThing&lt;Foo | Bar&gt; 而不是GenericThing&lt;Foo&gt;),即使在if 块内itemFoo!那么fooThing.item.fooProp; 的错误并不让我感到惊讶。

我猜 TypeScript 团队仍然需要做一些改进来支持这种情况。

【讨论】:

    猜你喜欢
    • 2021-10-12
    • 2020-02-16
    • 1970-01-01
    • 1970-01-01
    • 2016-03-11
    • 1970-01-01
    • 2020-12-03
    • 2020-04-12
    相关资源
    最近更新 更多