Proposal-pattern-matching: Possible ambiguity with TypeScript's "as" type assertion syntax

Created on 28 Apr 2021  路  3Comments  路  Source: tc39/proposal-pattern-matching

I'm continuing here the discussion about TypeScript's "as" type assertion syntax that was started in #188 (comment).

Since the expression following the pin operator affects the type of MatchResult, I assume type assertions like this would come up

const matcherA = {
  [Symbol.matcher](matchable) {
    return {
      matched: matchable === 'a',
      value: { a: 1 },
    };
  }
};
const matcherB = {
  [Symbol.matcher](matchable) {
    return {
      matched: matchable === 'b',
      value: { b: 2 },
    };
  }
};

type MatcherA = typeof matcherA
type MatcherB = typeof matcherB

function test(matchable: unknown, matcher: MatcherA | MatcherB) {
  // stuff...
  // Let's say we have somehow checked that after this point
  // matcher must be of type MatcherA, but TypeScript doesn't understand it
  // and still thinks it's MatcherA | MatcherB, so a type assertion is needed

  match (matchable) {
    when ^(matcher as MatcherA) as { a } {
      console.log(a.toFixed(2)) // a is inferred to be number here
    }
  }
}

That's a convoluted example, but given how prevalent the as type assertions seem to be in TypeScript code, I think it's safe to assume that they will be used inside match constructs too (if that's allowed in the first place).

Maybe in that case it's not a conflict in the sense there would be parsing ambiguity, but it could lead to some funny-looking code for sure. And, if we keep making this even more convoluted, ~I think we can arrive at some parser ambiguity too~:

type MatcherAFn = (matchable: unknown) => {matched: boolean, value: {b: number}}

match (matchable) {
  when ^(matcher as { [Symbol.matcher]: MatcherAFn }) { }
}

Is that a type assertion, telling TypeScript that matcher's type is { [Symbol.matcher]: MatcherFn }
or is it destructuring property Symbol.matcher from object {a: 1}?

Edit: Not an actual conflict, it would actually be parsed as the former (TS type assertion). See comments below.

Most helpful comment

Neither of these examples actually conflict, since the Typescript as is showing up in expression content, while the matcher as is showing up in matcher context, and ne'er the twain shall meet.

That is, the stuff within the ^(...) doesn't care about matcher syntax at all, it's just an arbitrary JS expression that needs to resolve to either a primitive or something with a Symbol.matcher property. So whatever TS stuff you put in there is between you and the TS compiler, and won't interfere with matcher syntax.

That said, I certainly understand how it could be confusing to read, with the two as keywords meaning entirely different things. That's always going to be a possible concern when a language is overlaid atop another still-developing language; there's no guarantee syntax claimed by the first won't end up getting used by the underlying one too.

However, if (as I'm strongly leaning in #188) we end up dropping as from pattern syntax entirely, in favor of just using the and/& combinator, then we end up avoiding the confusion issue as well, which is a minor note in favor of that approach.

All 3 comments

I believe TypeScript uses as only in expression position - which patterns aren't. So, when ^(foo as bar) as baz would work in typescript.

This does help the case for including the when parens (to differentiate expression position from the rebinding keyword).

In other words, in your example, it's strictly a type assertion. If you'd done the as outside the parens, it'd be a rebinding (which doesn't pay special attention to Symbol.matcher, since it's not a pattern) - ie, destructuring.

Neither of these examples actually conflict, since the Typescript as is showing up in expression content, while the matcher as is showing up in matcher context, and ne'er the twain shall meet.

That is, the stuff within the ^(...) doesn't care about matcher syntax at all, it's just an arbitrary JS expression that needs to resolve to either a primitive or something with a Symbol.matcher property. So whatever TS stuff you put in there is between you and the TS compiler, and won't interfere with matcher syntax.

That said, I certainly understand how it could be confusing to read, with the two as keywords meaning entirely different things. That's always going to be a possible concern when a language is overlaid atop another still-developing language; there's no guarantee syntax claimed by the first won't end up getting used by the underlying one too.

However, if (as I'm strongly leaning in #188) we end up dropping as from pattern syntax entirely, in favor of just using the and/& combinator, then we end up avoiding the confusion issue as well, which is a minor note in favor of that approach.

Given that there's no conflict (and that we'd strongly avoid creating one), I think this can be closed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

JLHwung picture JLHwung  路  3Comments

xtuc picture xtuc  路  4Comments

tabatkins picture tabatkins  路  6Comments

elijahdorman picture elijahdorman  路  6Comments

samuelgruetter picture samuelgruetter  路  3Comments