microsoft / TypeScript

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

`this` parameter not correctly inferred when unrelated type parameter has no inference candidates #56024

Open theajack opened 1 year ago

theajack commented 1 year ago

πŸ”Ž Search Terms

generics bug

πŸ•— Version & Regression Information

⏯ Playground Link

No response

πŸ’» Code

function test<
  Data1 extends Record<string, number>,
  Data2 extends Record<string, ()=>number>,
  Func extends Record<string, (this: Data2)=>void>,
>(options: {
  data1?: Data1,
  data2: Data2,
  func: Func,
}) {
    console.log(options);
};

test({
    data1: {
        a1: 1
    },
    data2: {
        a2 () {return 2;}
    },
    func: {
        f1 () {
            this.a2; // this.a2 has no type hints
        }
    }
});

πŸ™ Actual behavior

this.a2 has no type hints

πŸ™‚ Expected behavior

this.a2 has type hints

Additional information about the issue

If you comment out data1, then type hints appear

function test<
  Data1 extends Record<string, number>,
  Data2 extends Record<string, ()=>number>,
  Func extends Record<string, (this: Data2)=>void>,
>(options: {
  data1?: Data1,
  data2: Data2,
  func: Func,
}) {
    console.log(options);
};

test({
    // data1: {
    //     a1: 1
    // },
    data2: {
        a2 () {return 2;}
    },
    func: {
        f1 () {
            this.a2; // If you comment out data1, then type hints appear
        }
    }
});
theajack commented 1 year ago

I found that it works fine if I change it to an arrow function, but why doesn’t it work if I use the json abbreviation method?

function test<
  Data1 extends Record<string, number>,
  Data2 extends Record<string, ()=>number>,
  Func extends Record<string, (this: Data2)=>void>,
>(options: {
  data1?: Data1,
  data2: Data2,
  func: Func,
}) {
    console.log(options);
};

test({
    data1: {
        a1: 1
    },
    data2: {
        a2: () => {return 2;} // I found that changing it to an arrow function works fine.
    },
    func: {
        f1 () {
            this.a2;
        }
    }
});
theajack commented 1 year ago

It seems that the problem is the non-first generic parameter. If its corresponding parameter is the abbreviation of the json method, then the keyof of the json will be invalid. If it's the first parameter or if its method uses an arrow function, that's fine.

function test<
  Data1 extends Record<string, number>,
  Data2 extends Record<string, () => number>,
  Func extends (k1: keyof Data1, k2: keyof Data2)=>void,
>(options: {
  data1?: Data1,
  data2: Data2,
  func: Func,
}) {
    console.log(options);
};

const data = test({
    data1: {
        a1: 1
    },
    data2: {
        a2 () {return 2;}
    },
    func (k1, k2) {
    }
});

image

image

image

RyanCavanaugh commented 1 year ago

What is a "type hint" in your lexicon?

theajack commented 1 year ago

@RyanCavanaugh Like this, it will prompt me what properties are available on this image

RyanCavanaugh commented 1 year ago
function test<
  Data1 extends Record<string, number>,
  Data2 extends Record<string, ()=>number>,
  Func extends Record<string, (this: Data2) => void>,
>(options: {
  data1?: Data1,
  data2: Data2,
  func: Func,
}) {
    return options.data2;
};

// m1 and m2 have the same type, so Data2 is inferred identically
// in the return type, but `this` gets inferred to the constraint
// in m1 but to the object type in m2
const m1 = test({
//    ^?
    data1: { },
    data2: {
        a2 () { return 2; }
    },
    func: {
        f1 () {
            this.a2
            // ^?
            // this: Record<string, ()=> number>
        }
    }
});

const m2 = test({
//    ^?
    // data1: { }, // <- removed
    data2: {
        a2 () { return 2; }
    },
    func: {
        f1 () {
            this.a2
            // ^?
            // this: { a2(): number }
        }
    }
});