Hacker News new | ask | show | jobs
by int_19h 477 days ago
As far as I can tell, it's failing in the first conditional because it considers the type of `aOrB` to be `B` (rather than `A | B`) even before testing, based on it being initialized to B and then never assigned anything else. So when you apply the type guard, the resulting type of `aOrB` becomes, effectively, `A & B` (intersection), which of course doesn't have member `a`.

It would make more sense for TS to treat the body of the if-statement as unreachable and give a warning based on that, but I guess they figured that this kind of thing - doing a type guard on a value that is already known to be of a different type - is a very narrow corner case that isn't worth improving diagnostics for.

I'm more curious about why "smuggling" it through an array makes it work. In that case, the type of `aOrB2` remains `A | B` in the conditional, so everything is working as you expected, but I don't see the fundamental difference between this case and the previous one...

1 comments

> As far as I can tell, it's failing in the first conditional because it considers the type of `aOrB` to be `B`...

The playground hover type annotation says aOrB is `A | B` at declaration and if you hover over aOrB in `if (hasTypeName(aOrB, "A"))` it produces `const aOrB: B`. Two types for 1 variable with no operations between. Not clear what operation is being performed on `aOrB`'s type that transforms it or if the playground type hover is just wrong.

> It would make more sense for TS to treat the body of the if-statement as unreachable and give a warning based on that, but I guess they figured that this kind of thing - doing a type guard on a value that is already known to be of a different type - is a very narrow corner case that isn't worth improving diagnostics for.

That does not seem to be the case, the type guard is not guarding what is in the if block, at least not consistently. It is not about the value being known at least, it is about the property being missing from what I can tell and the guards not being able to guard against it. If you have a top level `a` and `b` in both `A` and `B` there are no errors triggered:

    type A = {
        type: {
            name: "A"
        }
        a: number,
        b: undefined,
    }

    type B = {
        type: {
            name: "B"
        }
        b: number,
        a: undefined,
    }


    const aOrB: A | B = {
        type: {
            name: "A"
        },
        a: 1,
        b: undefined
    };

    // error as expect
    if (aOrB.type.name === "B") {
        console.log(aOrB.b) // Error
    }

    function hasTypeName<Name extends string>(a: { type: { name: string }}, name: Name): a is { type: { name: Name }} {
        return a.type.name === name
    }

    if (hasTypeName(aOrB, "B")) {
        console.log(aOrB.b) // no error
    }

    if (hasTypeName(aOrB, "A")) {
        console.log(aOrB.a) // no error here as well
    }
The guards not working or premature type narrowing(the inability to set a variable to a type and have typescript treat it as that type with the above type annotations).
> The playground hover type annotation says aOrB is `A | B` at declaration and if you hover over aOrB in `if (hasTypeName(aOrB, "A"))` it produces `const aOrB: B`. Two types for 1 variable with no operations between.

It's not that the variable has two different types. It's that the expression `aOrB` has a different type inside the condition. This is normal for TS - indeed, the very pattern of doing a check first and then magically getting a different type inside the body of the conditional hinges on this narrowing behavior. This particular case just looks a bit weird because there's no conditional, it's based solely on the assignment. You can see the same in code without any conditionals at all:

  let foo: {foo: number} | {bar: string};
  foo = {foo: 123};
  foo; // if you hover over foo here, the type is narrowed.
So, before it even gets to the type guard, it has already determined that the actual type of expression `aOrB` can only be `B`, and typed it as such. OTOH when a type guard is used, if it returns true, it knows that `aOrB` can only be `A`. To combine these two, it has to type it as `A & B`, which is what you see in the hover if you do it inside the body of the conditional. And the intersection type will only show the properties `A` and `B` have in common.

As for your new example, keep in mind that a missing property is not the same as `undefined` in TS (nor in JS itself, since there are ways to observe that difference). So the sum type must have both `a` and `b`, but either one can be set to `undefined` (but not omitted!) depending on `type`. If you remove `b: undefined` from the initializer of `aOrB`, you will see an error telling you that `b` is missing.

However, your example does not produce any error at the line with the comment that says "Error". Instead, you get a warning on the line above, specifically for this expression:

   aOrB.type.name === "B"
And if you look at the text of that warning, it basically says that `aOrB.type.name` is statically known to always be of type "A" at this point (since that is what was in your initializer, and TS did the requisite narrowing), and thus comparing it to "B" is pointless since it'll never be equal. All the property accesses for `a` and `b` work fine though since your sum type has both properties for both variants.