close
Skip to content

Design Meeting Notes, 1/3/2024 #56942

@DanielRosenwasser

Description

@DanielRosenwasser

Intersections and Unions that Depend on Instantiation Order

#56906

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 for any.
    • Problem is that we say SomeUninstantiatedTypeVariable | unknown, we eagerly reduce that to unknown.
    • We would need to defer union reduction in the presence of generics and unknown or any.
      • 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.
  • 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

#56780

  • 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.
  • 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

#56908

  • 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 for var 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;
        }
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design NotesNotes from our design meetings

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions