Typescript: Unable to discriminate union type because of "Property does not exist"

Created on 25 Oct 2018  路  5Comments  路  Source: microsoft/TypeScript

TypeScript Version: [email protected]


Search Terms: union narrowing discriminate missing property field

Code

class Foo
{
    private foo: string;
}

class Bar
{
    private bar: number;
}

function fn(baz: { foo: Foo; bar: string; } | { foo: Bar; })
{
    if (typeof baz.bar != 'undefined')
    {
        testFoo(baz.foo);
    }
}

function testFoo(foo: Foo)
{
}

Expected behavior:
No Error. I can call testFoo(baz.foo) since the union type is properly narrowed.

Actual behavior:
Property 'bar' does not exist on type 'Baz'.

Argument of type 'Foo | Bar' is not assignable to parameter of type 'Foo'.
Type 'Bar' is not assignable to type 'Foo'.
Property 'foo' is missing in type 'Bar'.

Playground Link: https://www.typescriptlang.org/play/index.html#src=class%20Foo%0D%0A%7B%0D%0A%20%20%20%20private%20foo%3A%20string%3B%0D%0A%7D%0D%0A%0D%0Aclass%20Bar%0D%0A%7B%0D%0A%20%20%20%20private%20bar%3A%20number%3B%0D%0A%7D%0D%0A%0D%0Afunction%20fn(baz%3A%20%7B%20foo%3A%20Foo%3B%20bar%3A%20string%3B%20%7D%20%7C%20%7B%20foo%3A%20Bar%3B%20%7D)%0D%0A%7B%0D%0A%20%20%20%20if%20(typeof%20baz.bar%20!%3D%20'undefined')%0D%0A%20%20%20%20%7B%0D%0A%20%20%20%20%20%20%20%20testFoo(baz.foo)%3B%0D%0A%20%20%20%20%7D%0D%0A%7D%0D%0A%0D%0Afunction%20testFoo(foo%3A%20Foo)%0D%0A%7B%0D%0A%7D

Related Issues: I've looked around but found none.

Although, in general, I do understand this should not work, I think it would help greatly if we had a way to discriminate on properties that are only in some of the members of a union.

One of the solutions could be to type properties, that are missing in some of the union members as prop?: void, or prop?: never, or prop?: undefined, or whatever makes sense in the type system.

Question

Most helpful comment

Although, in general, I do understand this should not work, I think it would help greatly if we had a way to discriminate on properties that are only in some of the members of a union.

This should work

function fn(baz: { foo: Foo; bar: string; } | { foo: Bar; })
{
    if ("bar" in baz)
    {
        testFoo(baz.foo);
    }
}

With this PR #27695 you can also do this:

function fn(baz: { foo: Foo; bar: string; } | { foo: Bar; bar: undefined })
{
    if (baz.bar !== 'undefined')
    {
        testFoo(baz.foo);
    }
}

All 5 comments

Although, in general, I do understand this should not work, I think it would help greatly if we had a way to discriminate on properties that are only in some of the members of a union.

This should work

function fn(baz: { foo: Foo; bar: string; } | { foo: Bar; })
{
    if ("bar" in baz)
    {
        testFoo(baz.foo);
    }
}

With this PR #27695 you can also do this:

function fn(baz: { foo: Foo; bar: string; } | { foo: Bar; bar: undefined })
{
    if (baz.bar !== 'undefined')
    {
        testFoo(baz.foo);
    }
}

@jack-williams Your suggestion works. Thank you!

I'll keep this issue open as I think it should be documented and/or we could have better syntax for it... but I might be wrong. I'd like a member of TS team to have a look and provide a definitive decision on that if you don't mind...

Yep, with what we have in master now, you should just need to actually include the member you want to discriminate on as an (optional) undefined value for all union members it otherwise wouldn't exist on.

BTW, there's a good reason this:

function fn(baz: { foo: Foo; bar: string; } | { foo: Bar; })
{
    if (typeof baz.bar != 'undefined')
    {
        testFoo(baz.foo);
    }
}

doesn't work - consider this call:

fn({bar: 12, foo: new Bar()});

that's a valid call (assignable to the second union member), and is why even if you were to check that bar wasn't undefined in the input, you might still not have a Foo for foo. The in operator is intentionally a little unsound here, but we figured that its usage was infrequent enough that if you use it you really wanted it to act like this.

Indeed. I haven't thought of this case.
I guess it would impossible to implement it in a completely sound way.

Thanks for your answer!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dlaberge picture dlaberge  路  3Comments

uber5001 picture uber5001  路  3Comments

siddjain picture siddjain  路  3Comments

bgrieder picture bgrieder  路  3Comments

remojansen picture remojansen  路  3Comments