Skip to content

Design Meeting Notes, 2026-06-04 #63533

@RyanCavanaugh

Description

@RyanCavanaugh

as/satisfies precedence and erasableSyntaxOnly

Issue: #63527

  • Problem: 1 + 2 as number / 3 parses as ((1 + 2) as number) / 3
  • This can't be emitted without parenthesizing 1 + 2, since 1 + 2 / 3 has different meaning
  • This in turn requires "moving" 1 + 2 over by one to make room for the opening paren
  • Bug report: Therefore this should be banned under erasableSyntaxOnly (ESO)
  • Question 1: Does the violate the implied constraints of ESO?
    • Argument against: No, ESO was only about removing annotations, and this is still a removal of type info. "blankability" was never a discussed property when designing ESO.
    • Argument for: Many current ESO type strippers, e.g. ts-blank-space, treat this as an invariant for the sake of reusing source maps
  • Conclusion 1: Preserving source maps is a very useful invariant, and all other ESO limitations have this property. Adopt the "new" interpretation where ESO implies that you can type-strip by space substitution without moving anything around
  • Question 2: Isn't this precedence awful? Does anyone writing 1 + 2 as number / 3 actually expect this expression to evaluate the way it does?
  • Also applies to satisfies
  • This is generally not a live question because this really only matters for binary computation operators, which rarely need type assertions, and this code looks odd enough already that you're likely to add parens anyway. Nevertheless.
  • There's modern JS precedent for erroring on "too-confusing precedence" situations, e.g. -2 ** 3
  • Conclusion 2: Make this an error in all configurations, assuming a top800 run produces reasonable results

Template literals and Unicode

Issue: microsoft/typescript-go#4137

  • Problem: Consider this code
type Heads<S> = S extends `${infer C}${infer R}` ? [C, R] : never;
type A = Heads<"😀abc">;
declare let a: A;
const chk: ["😀", "abc"] = a;

Corsa says this code is OK, which seems reasonable. This is consistent with e.g. [..."😀abc"], or for (const c of "😀abc") {, or /^./u.exec(.

Strada splits "😀" into \uD83D and \uDE00, which seems a bit less reasonable. This is consistent with "😀abc"[0] or "😀abc".split(""), or /^./.exec(, but generally when this actually happens in a way that matters, it's a bug.

Should we change this? The usefulness of splitting these surrogates apart is... questionable. You end up with A here as an illegal unpaired surrogate, which we can't even send over the wire or serialize without special encoding. Re-joining strings that might have isolated surrogates requires extra work to detect and re-join them. At best this is exposing an implementation detail of JS.

The downside of changing this that a Peano-computed Length type primitive will turn up a different number than the JS .length property, but this is already a hazard in JS ("😀".length !== [..."😀"]).length).

Conclusion: Change template literal inference to split on runes instead of code points. Run top800 and see what happens - if OK, accept this as a breaking change for 7.0.

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