Open DanielRosenwasser opened 1 year ago
I think the code in question for pyright was introduced in:
Wait, does TS walk the control flow graph backwards to discover narrowings? I always figured the graph looked something like x = string | number
-> if (true) x = string
-> else x = number
and the compiler would know exactly which part of the tree it was in at the time it encountered a reference to x
so it could just look up the current type directly.
It does walk backwards. If it walked forwards, it would have to retain the context of all potential narrowing operations (and for all potential types we'd want to narrow from - which is occasionally necessary for things like narrowing constraints of generics rather than the generics themselves). That wouldn't work in our system where checking an expression should be as lazy as possible.
Anyway, in line with that, there are definitely opportunities for us to optimize here.
I guess that makes sense now that I think about it - there really aren't any "set the type to string
" style narrowings - even typeof x === 'string'
can narrow to never
if the original type doesn't overlap with string
. Otherwise you could probably do some kind of short-circuiting logic.
In Pyright, narrowing can be avoided by keeping track of which entities actually get narrowed within a given scope. This could save a good amount of time by avoiding redundant walks up the control flow graph just to discover nothing interesting a given variable or property.
I'm not sure if Pyright also tracks the first location at which an entity may be narrowed, but that information could also be used to signal when a narrowing walk needs to stop.
This would come at the cost of some memory overhead, but we'd need to experiment to see what the trade-offs are.