Discriminated Union of Generic type

吃可爱长大的小学妹 提交于 2020-07-15 02:43:10

问题


I'd like to be able to use union discrimination with a generic. However, it doesn't seem to be working:

Example Code (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!
    }
}

I was hoping that typescript would recognize that since I discriminated on the generic item property, that genericThing must be GenericThing<Foo>.

I'm guess this just isn't supported?

Also, kinda weird that after straight assignment, it fooThing.item loses it's discrimination.


回答1:


The problem

Type narrowing in discriminated unions is subject to several restrictions:

No unwrapping of generics

Firstly, if the type is generic, the generic will not be unwrapped to narrow a type: narrowing needs a union to work. So, for example this does not work:

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;
    }
}

While this does:

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

I suspect unwrapping a generic type that has a union type argument would cause all sorts of strange corner cases that the compiler team can't resolve in a satisfactory way.

No narrowing by nested properties

Even if we have a union of types, no narrowing will occur if we test on a nested property. A field type may be narrowed based on the test, but the root object will not be narrowed:

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;
    }
}

The solution

The solution is to use a custom type guard. We can make a pretty generic version of the type guard that would work for any type parameter that has a type field. Unfortunately, we can't make it for any generic type since it will be tied to 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;
  }
};



回答2:


It's a good point that the expression genericThing.item is seen as a Foo inside the if block. I thought that it works only after extracting it to a variable (const item = genericThing.item). Probably a better behaviour of latest versions of TS.

This enables the pattern matching like in the function area in the official documentation on Discriminated Unions and that is actually missing in C# (in v7, a default case is still necessary in a switch statement like this).

Indeed, the weird thing is that genericThing is still seen undiscriminated (as a GenericThing<Foo | Bar> instead of GenericThing<Foo>), even inside the if block where item is a Foo! Then the error with fooThing.item.fooProp; does not surprise me.

I guess the TypeScript team has still some improvements to do to support this situation.



来源:https://stackoverflow.com/questions/50870423/discriminated-union-of-generic-type

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!