Repository navigation
Inferring Intersection Types #8911
Description
Activity
Type guards don't magically widen types. The only narrow types. You made it clear that
ais onlyA. Just because it has excess properties, doesn't make itA & B.If you wanted it to work, you would have to let the compiler know that
acould possibly beA & B:// two interfaces interface A { a: string; } interface B { b: string; } // a type guard for B function isB(toTest: any): toTest is B { return toTest && toTest.b; } // a function that turns an A into an A & B function union(a: A | (A & B)): A & B { if (isB(a)) { return a; // compiler correctly assumes `a` is not just `A` but is `A & B` and // gives no error } else { return null; } }
Hm. Is there an issue with soundness?
I know that
ais anAbecause it's declared so in the signature. I know that it's also aBbecause the type guard says so. If something is anAand is also aBis it not safe to say it's anA & B?Yes, but you said
ais onlyA, or that you are contracting to only deal with anAstructure with theaparameter. I guess with flow control, inside of the scope of the guard, the way you originally wrote it, it should be the new bottom typenever, as you have said it is notAanymore. TypeScript is being permissive there at the moment. I haven't looked at what it would do under flow control though, but the TypeScript team would know better than I.Why does it have to stop being an
A?Lee Avital (@leeavital) Rewrite your type guard like so for the behavior you want:
interface A { a: string; } interface B { b: string; } // a type guard for B function isB<T>(toTest: T): toTest is (T&B) { return toTest && (toTest as any).b; } // a function that turns an A into an A & B function union(a: A): A & B { if (isB(a)) { return a; } else { return null; } }
TS only narrows a type to a subtype - it never narrows 'across', as it were.
With the nightly build compiler, the type of
aisneverunder theisB(a)type guard. In 1.8 we didn't have anevertype and we'd instead revert to the declared type.The operating principle for type guards is that they only "narrow" the type, i.e. they only make the type more specific. Type guards never change the type to something unrelated such as from
AtoB. If they did you'd end up with odd effects such asanot being assignable to itself.I think you're suggesting we should allow "narrowing across" to unrelated types and then produce an intersection of the declared type and the tested-for type (i.e.
A & Bin your example). Effectively this would guarantee that the resulting type is always a more specific type and always assignable to the declared type. The flip side is that it would produce nonsense types for nonsense type guards instead of producingnever. I think thenevertype is a clearer indication that something is amiss.- addedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
on Jun 7, 2016 - addedSuggestionAn idea for TypeScriptAn idea for TypeScriptand removedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
on Jun 8, 2016 Reopening as suggestion. More examples of this feature in #9016.
I'm coming around to the view that we should produce intersection types in type guards for unrelated types. In particular, given that
isA(obj) || isB(obj)narrows the type ofobjtoA | Bin the true branch, it seems very reasonable thatisA(obj) && isB(obj)should narrow toA & B.It's a pretty simple change. I will look at putting up a PR.
- locked and limited conversation to collaborators
on Jun 19, 2018
TypeScript Version:
1.8.10
Code
Expected behavior:
I would expect this to compile successfully.
Actual behavior:
Inside the if-block, tsc has "forgotten" about the A-ness of
a. This is a useful pattern ifbis an expensive property to compute and you want to do different things ifbis or is not present.