Do type guards only narrow the type when true is returned?

Viewed 75

I was under the impression that a type guard which takes multiple types instanceOfA(arg: A | B | C): arg is A, would narrow the type to either A (when the guard returns true) or B | C (when the guard returns false)

However in the case of instanceOfB below, returning false seem to narrow the type to never, while true doesn't narrow it at all. Do type guards in fact only narrow the type if true is returned? Or am I misunderstanding the results, in which case, why does the narrowing to never occur?

// Structures
interface A {
    value: string
    children: B[]
}

interface B {
    value: string
}

// Example functions
function doSomething1(arg: A | B) {
    if (instanceOfA(arg)) {
        console.log(`A: ${arg.value}`) // Type of arg is A
    } else {
        console.log(`B: ${arg.value}`) // Type of arg is B
    }
}

function doSomething2(arg: A | B) {
    if (instanceOfB(arg)) {
        console.log(`B: ${arg.value}`) // Type of arg is A | B
    } else {
        console.log(`A: ${arg.value}`) // Type of arg is never
    }
}

// Typeguards
function instanceOfA(ab: A | B): ab is A {
    return ab.hasOwnProperty('children')
}

function instanceOfB(ab: A | B): ab is B {
    return !ab.hasOwnProperty('children')
}
1 Answers

The question is, is this a theoretical question or are you having a real-world issue? if it's the second, I'd say it's just wrong identifying a type by what property it doesn't have.

If you read the narrowing section in the typescript book they have an example where they use a type guard to differentiate a Fish from a Bird by checking if the pet can swim (which in itself is a quite generous assumption). But what you are doing (transformed to the bird, fish example) is basically saying: "If it's can't swim it must be a bird". Doing that just doesn't seem particularly logical (as pointed out in the comments that this assumption is valid if there are only fish and birds to choose from).

I don't think the instanceOfB is sensible. TypeScript allows assigning different types to each other if they have enough overlap, and A will always be assignable to B because it completely covers all properties it has. As soon as you add some property to B that A doesn't have the issue will go away.

For example this would be perfectly fine typescript:

const a: A = {value: 'this is a', children: []};
const b: B = {value: 'this is b'};

function printValue(subject: {value: string}) {
    console.log(subject.value);
}

And your check would start working if you did this (for example):

interface B {
    value: string
    other: string;
}
Related