Search code examples
typescript

Why can't I narrow types using property null checking?


Why can't I use property checking to narrow the type like so?

function test2(value:{a:number}|{b:number}){
    // `.a` underlined with: "Property a does not exist on type {b:number}"
    if(value.a != null){
        console.log('value of a:',value.a)
    }
}

test2({a:1})

test2({b:2})

It transpiles fine, works as designed, and throws no runtime errors. The property check logically narrows the scope to {a:number}.

So what's wrong with this? Is there a setting I can change to make it allow and use this method of type narrowing?

Someone will probably tell me to use the in operator. So I suppose I'll say now that I know I could use the in operator, but I don't want to. If I union the type with string as well, then I can't use in with string and I have to first check to make sure it's not a string with if(typeof value != 'string'), but in regular JavasScript I could simply leave my property check and know that whatever I get will have the property and all will be well.

So, what's the problem with TypeScript here? Is it just not smart enough to figure it out?

(Changes: This question was originally about optional chaining to check for properties because the type was unioned with a null as well, but to simplify the question I've removed the null and the optional chain operator from the if statement.)


Solution

  • what's the problem with TypeScript here?

    It just points out diligently that your code is accessing a property that is not declared to exist on the object. If it wasn't a union type, you'd expect to get this error message. That the code doesn't throw a runtime error doesn't mean that it's valid TypeScript.

    If you insist on accessing the property to discriminate the union, you'll have to declare it on all type constituents - you still can declare it to have no value on the others:

    function test2(value: { a: number; b?: never } | { b: number; a?: never }){
        if(value.a != null) {
            console.log('value of a:', value.a)
        }
    }
    
    test2({a:1})
    test2({b:2})