microsoft/typescript-go

tsc vs tsgo: inference mismatch of parameter in passed callback

Closed this issue · 5 comments

Steps to reproduce

mkdir tsgo-vs-tsc
cd tsgo-vs-tsc
npm init -y
npm pkg set type=module
npm i typescript @typescript/native-preview css-tree @types/css-tree -D
npx tsc --init --module nodenext --moduleResolution nodenext
printf "import { parse, walk } from 'css-tree';\nconst declarationsText = '...';\nconst ast = parse(declarationsText, { context: 'declarationList', positions: true });\n\nwalk(ast, {\n    visit: 'Declaration',\n    enter(node) {\n        console.log(node);\n    },\n});" > main.ts
npx tsc --noEmit
npx tsgo --noEmit

In case your terminal doesn't support printf, the contents of main.ts:

import { parse, walk } from 'css-tree';
const declarationsText = '...';
const ast = parse(declarationsText, { context: 'declarationList', positions: true });

walk(ast, {
    visit: 'Declaration',
    enter(node) {
        console.log(node);
    },
});

Behavior with typescript@5.8

No type errors. node is inferred as Declaration.

Behavior with tsgo

main.ts:7:11 - error TS7006: Parameter 'node' implicitly has an 'any' type.

7     enter(node) {
            ~~~~


Found 1 error in main.ts:7

This is an effect of #1085.

The second argument to walk is contextually typed by type EnterOrLeaveFn | WalkOptions. WalkOptions in turn is a union of a bunch of WalkOptionsVisit<XXX> types and WalkOptionsNoVisit. When this union is narrowed by the discriminant visit: 'Declaration', the old compiler produces the contextual type WalkOptionsVisit<Declaration>, but, due to #1085, the new compiler produces the contextual type WalkOptionsVisit<Declaration> | WalkOptionsNoVisit. The type that includes WalkOptionsNoVisit is correct because WalkOptionsNoVisit doesn't include a visit discriminant property and thus can't safely be eliminated (it acts as if there was a visit: unknown discriminant). This in turn means that the enter method is contextually typed by EnterOrLeaveFn<Declaration> | EnterOrLeaveFn<CssNode>, and since contextual typing by a union type only takes effect when the union constituents have identical parameter types, we end up not contextually typing the node parameter.

I'm not quite sure what the purpose of including the WalkOptionsNoVisit type in the WalkOptions union is, but removing it or adding a visit: never property produces the intended behavior.

The error also occurs in typescript@5.9 and later as a result of microsoft/TypeScript#61828 which ports #1085 to the old codebase.

Thank you for looking at and explaining this.

I'm cannot see this error on typescript@5.9.2 (the steps to reproduce use it as well).

Looking at the release/5.9 branch, I can see that the PR did get included, so perhaps something happened during the porting process which made this apply differently.

@AviVahl Hmm, yes, you're right, it doesn't reproduce with typescript@5.9. There are actually several issues revealed here.

First, due to a translation error, an optimization we do for discriminated unions with more than 10 cases hasn't actually been working in tsgo. Second, enabling the optimization reveals another code path that should have been changed in #1085.

Because of the disabled optimization, #1085 affected discriminated unions of any size in tsgo, but only discriminated unions with less than 10 cases in tsc.

So, we need two PRs: One that enables the large discriminated union optimization and fixes that code path according to #1085 in tsgo, and another ports the large discriminated union code path fix to tsc. Once those PRs are in, you'll see the same behavior for this repro in both compilers (namely, an error on the repro in this issue).

Relabeling as bug, but with fix as described here.