Description
Intersections and Unions that Depend on Instantiation Order
type Union<A, B> = A | B
type UnionAny<T> = Union<T, any>
type UnionUnknown<T> = Union<unknown, T>
// these should all be the same type but are respectively: any, any, unknown
type UA0 = Union<unknown, any>
// ^?
// any
type UA1 = UnionAny<unknown>
// ^?
// any
type UA2 = UnionUnknown<any>
// ^?
// unknown
any
"eats" everything.unknown
does too, except forany
.- Problem is that we say
SomeUninstantiatedTypeVariable | unknown
, we eagerly reduce that tounknown
. - We would need to defer union reduction in the presence of generics and
unknown
orany
.- That would be very annoying - you wouldn't be able to say that unioning with the top type(s) just reduces to the top type.
- [[Editor note]]: probably also have to thread through contexts where you have to know when to reduce the type.
- Problem is that we say
- It certainly is an inconsistency, but did it come up in a practical context?
- That is unclear.
- Conclusion: we admit that it is inconsistent and undesirable, but we are not fully convinced that it needs to change.
Disallowing More Property Access Operations on never
-Typed Expressions
-
From a purely type-theoretic standpoint, there is no issue with accessing a property on
never
, the issue is arguably more that the code is unreachable. -
Could argue that you should say "this whole block is unreachable, so why do you have code?"
-
That is too heavy-handed - the following doesn't deserve an error, does it?
function foo(x: "a" | "b" | "c"): string { switch (x) { case "a": case "b": case "c": return "hello"; default: throw Debug.fail("Wrong value " + x); } }
-
Two common views of
never
are- "Consuming" a value of type
never
to enforce an exhaustiveness check. - "Probing" a value of type
never
to report a helpful error for a violation of the type system or usage from untyped code.
- "Consuming" a value of type
-
In practice, this change actually breaks a bunch of existing code.
-
Conclusion: feels like we don't have an appetite
Preserving Type Refinements Within Closures Past Their Last Assignments
-
Fixes lots of issues that get duped to Trade-offs in Control Flow Analysis #9998.
-
When a closure is created after all assignments to some closed-over variable, then you can effectively think of that variable as unchanging.
-
Effectively safe to preserve the type-assignments so long as the closure isn't hoisted.
- Means things like object methods, function expressions, and class expressions can observe prior type narrowing/refinment.
- In theory, class declarations could also be made to work here.
- We noticed that class declarations were considered "hoisted"...but they're not!
-
Note that we don't quite narrow in a lexical manner within "compound statements". We use the "end" of the compound statement as the source of the last assignment.
function f5(condition: boolean) { let x: number | undefined; if (condition) { x = 1; action(() => x /*number | undefined*/) } else { x = 2; action(() => x /*number | undefined*/) } // <- we consider this to be the position of the last assignment of 'x' action(() => x /*number*/) }
- This approach lends to a fast and efficient check.
- It would also be tricky in the case of loops, where the "last" assignment cannot truly be determined.
-
Variables that are referenced in closures past the last assignment do not necessarily trigger an implicit any warning anymore.
let x; x = 42; doSomething(() => x)); // ~ // Implicit any error! ❌ x = "hello!"; doSomething(() => x)); // No error! ✅
-
If we limit to
let
and parameters, then we don't have any issues with our perf suite - but it does forvar
on monaco.- We might have other analyses we can apply.
-
Function declarations in block scopes should maybe have captured variables appropriately narrowed.
function f(x: string | number) { if (typeof x === "number") { function g() { x // should this be number, but is not in the PR } return g; } }