区分通用类型的联盟

时间:2018-06-15 06:46:49

标签: typescript discriminated-union

我希望能够将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!
    }
}

我希望打字稿能够识别出由于我对通用item属性进行了区分,因此genericThing必须是GenericThing<Foo>

我猜这只是不支持?

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

3 个答案:

答案 0 :(得分:8)

问题

受歧视的工会中的类型缩小受到若干限制:

不会删除泛型

首先,如果类型是通用的,则不会解开泛型以缩小类型,缩小需要联合才能工作。例如,这不起作用:

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;

    }
}  

答案 1 :(得分:0)

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

这样可以在Discriminated Unions上的官方文档中的函数area中进行模式匹配,并且在C#中实际上缺少这种模式(在v7中,default情况下仍然需要这样的switch声明。)

确实,奇怪的是genericThing仍被视为不加区别(GenericThing<Foo | Bar>而不是GenericThing<Foo>),即使在ifitem内也是如此是Foo!那么fooThing.item.fooProp;的错误并不会让我感到惊讶。

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

答案 2 :(得分:0)

正如@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<...> 表示的类型需要是 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