microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.87k stars 12.36k forks source link

`getSignaturesOfType` throws `Cannot read property 'flags' of undefined` #33829

Open ark120202 opened 4 years ago

ark120202 commented 4 years ago

TypeScript Version: 3.7.0-dev.20191005

Search Terms: conditional, infer, mapped

Code

declare interface Foo {
    a(): boolean;
    b(): void;
    c(argument: boolean): void;
    d(argument: number): void;
}

declare const Foo: Mapped<Foo>;
type Mapped<T> = {
  [K in keyof T]: T[K] extends (...args: infer Args) => infer R
    ? (...args: Args) => R
    : never;
};

function foo(key: 'a' | 'b' | 'c' | 'd') {
  Foo[key]();
}

Note: removing any infer declaration or method from Foo makes it valid.

Expected behavior:

No errors

Actual behavior:

TypeError: Cannot read property 'flags' of undefined
    at Object.getCheckFlags (typescript/lib/tsc.js:10461:23)
    at getTypeOfSymbol (typescript/lib/tsc.js:32706:20)
    at getParameterCount (typescript/lib/tsc.js:46831:32)
    at combineUnionParameters (typescript/lib/tsc.js:33697:29)
    at combineSignaturesOfUnionMembers (typescript/lib/tsc.js:33730:26)
    at typescript/lib/tsc.js:33673:181
    at Object.map (typescript/lib/tsc.js:446:29)
    at _loop_7 (typescript/lib/tsc.js:33673:144)
    at getUnionSignatures (typescript/lib/tsc.js:33681:35)
    at resolveUnionTypeMembers (typescript/lib/tsc.js:33754:34)
    at resolveStructuredTypeMembers (typescript/lib/tsc.js:34078:21)
    at getSignaturesOfStructuredType (typescript/lib/tsc.js:34573:32)
    at getSignaturesOfType (typescript/lib/tsc.js:34579:20)

Playground Link: https://www.typescriptlang.org/play/?ts=3.7-Beta&ssl=1&ssc=1&pln=18&pc=2#code/CYUwxgNghgTiAEBLAdgFxDAZlMCBiA9gfAN4CwAUPNfFABQCUAXPAEZEQhTIDclNbRiwBuBRMD5UaYOrADmAVwC2INC3YFO3ZvFHjJA4LJiKVa+MmWsMOvRMoBfSpVCRYCMAWQBnVPEIELACyUAAOoSDAADwBAHySqACeEfAh4ZFRACqx8AC8pPzwANoA0kjI8ADWIIkEmPCZALosmaWN8CAAHujIwN7wdAB0w-LeLCiYGPAAgibeDHk5E1MASoXUAPwDw4OjLLNy84vwa1LULMggwhiSDpKUmArIYKiIXvCYRHTViSwA5FA-vAAD7wP6sIGgv5gSFg4B-BbkKQBIo-RqMW5AA

Related Issues:

jasonk commented 2 years ago

I ran into this issue and after fighting with it for a while managed to boil it down to a minimal reproduction: https://github.com/jasonk/repro-ts-33829

What I've managed to determine is that combineUnionParameters is getting called to merge the parameter types of some functions. To do that it starts by calling getParameterCount on the signatures of the two functions:

        function getParameterCount(signature) {
            var length = signature.parameters.length;
            if (signatureHasRestParameter(signature)) {
                var restType = getTypeOfSymbol(signature.parameters[length - 1]);
                if (isTupleType(restType)) {
                    return length + restType.target.fixedLength - (restType.target.hasRestElement ? 0 : 1);
                }
            }
            return length;
        }

In all of the examples I've seen what happens here is that signatureHasRestParameter(signature) returns true while signature.parameters is an empty array. So then it ends up calling getTypeOfSymbol( undefined ) which then throws the Cannot read properties of undefined error.

I'm not all that familiar with the internals of TypeScript but from what I found while trying to narrow it down to a reproduction I suspect that the problem is that if you have circular imports you can end up in a situation where it's trying to merge function parameter types at a point where it's already determined that one ore more of those functions have a rest parameter, but hasn't yet populated the parameters list in that signature object..

Yesterday17 commented 2 years ago

I managed to make the example work with the following workaround (Playground):

declare interface Foo {
    a(): boolean;
    b(): void;
    c(argument: boolean): void;
    d(argument: number): void;
}

declare const Foo: Mapped<Foo>;
type Mapped<T> = {
  [K in keyof T]: T[K] extends (...args: infer Args) => infer R
-   ? (...args: Args) => R
+   ? Args extends [any, ...any[]]
+      ? (...args: Args) => R
+      : () => R
    : never;
};

function foo(key: 'a' | 'b' | 'c' | 'd') {
  Foo[key]();
}

It seems that TypeScript compiler can not expand inferred arguments correctly on functions which does not have parameters at all.