microsoft / TypeScript

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

Return type inference fails on classes derived from generic types without explicit reference to the types #60443

Open manbearwiz opened 5 hours ago

manbearwiz commented 5 hours ago

🔎 Search Terms

"generic type inference"

🕗 Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.3#code/MYGwhgzhAEBCD2APAPAFWgU0QFwwOwBMYAlDYeAJwOQmwoEs8BzAGmgFc8BrPeAdzwA+QdADeAKGjQA9NOgBVPOQC2y-NmjYAFhmgAjDCH6b40RqHYFdYaBQwAzDHaW7sp9I007oTfE-rA0KCQMGCE0Pb0iLYY2OwUeJoAngAOuoyOzsAYkjJyKQwAbmC40AD6ZZwqanjYjExlkYgYEBUA-ABc0KgA3OK5vtgAIiVgAGIU8MoAylMYfDp2AKIgEBjIANKYOPhE0FwYSfD23YIAFAdJXRsAlF2oANobALpiuVLkeBDwIBgAdEYmGcAOSXYFsS43PpSKSyMzKFK-GrYd4xOIJaB4dggEAAQmh0AAvuJieJsKldNM4vYTgBeN5SezweBdUTQMBdLHKAwUIkEvRgCis-RdWgMZh8kl9YJQaDTNLAehgEAIaJYXCEGCq5BU9g0kSiYmfWjQCAKpUgaD0vDzOXm5Wqs5Q8TGjR6JBWzG27W6-VOvriexVOrwRJ8ChgFIAcViuAoyFyqEFg22Gr22tI5CoNDo9TYnB4-CEghYuS26t2MCTFBTFc1cCQyAyTmgAE0RG19odjm3oJyMIUnKWpPJU5XusnYmP69rm7yAFod6Dzp6vfuDiil87wPQAK3uk+wEMO1zuCgZaPiiR3u7+gxG2HGkxmcwWTgwKzWF0ON3ZMHkfSknC96jBMUyzGob7LKs1jhOGkYxtgcbIAAwqGj6ME4Ih8JQXARJhbB6OwGjwdGsZODq9oqkgIj2GA9CrC6obfL8ALwEC7qIHesQPk+4GvosH4wSCTLwMCNx-GAbCkYhcZnJxbDAqJ4mSdJEZkUhThnGaZAWopykSWAzrGj8-yAvJSDccMoHPhB8yCZ+GAggKFAqXoakIeRFAWYgikuW5HkaXJOmKsqfmCm5zpAA

💻 Code

class Box<T extends Record<string, unknown>> {
  // Uncomment the below to include a reference to T in the generic class and fix return type inference
  // private __uncommenting_fixes__?: T;

  getDataFromSomewhereElse<K extends keyof T>(key: K): T[K] {
    console.log('key', key);
    // implement
    return null!;
  }
}

type Stuff = {
  foo: { a: number };
  bar: { b: string };
};
class SpecialBox extends Box<Stuff> {}
const special = new SpecialBox();
const box = new Box<Stuff>();

function wrapGetter<
  Target extends Box<Record<string, unknown>>,
  K extends Target extends Box<infer Y> ? keyof Y : never,
  U extends Target extends Box<infer Z> ? Z[K] : never,
>(obj: Target, key: K): U {
  return obj.getDataFromSomewhereElse(key) as U;
}

// getDataFromSomewhereElse and wrapGetter<Container> work fine, but wrapGetter<SpecialBox> fails
console.log(box.getDataFromSomewhereElse('foo').a, wrapGetter(box, 'foo').a, wrapGetter(special, 'foo').a);
console.log(box.getDataFromSomewhereElse('bar').b, wrapGetter(box, 'bar').b, wrapGetter(special, 'bar').b);

🙁 Actual behavior

In the playground, you will see that ts cannot infer the return type of wrapGetter when passed special that was instantiated with special = new SpecialBox(). It flags it as an error with Object is of type 'unknown'.

However, wrapGetter(box, 'foo').a has no issue inferring the return type since box is instantiated like box = new Box<Stuff>().

Uncommenting private __uncommenting_fixes__?: T allows both calls to successfully infer the return type.

🙂 Expected behavior

I would expect wrapGetter to work the same arguments of type Box<Stuff> or SpecialBox extends Box<Stuff>.

Additional information about the issue

No response

RyanCavanaugh commented 2 hours ago

There's no structural inference site of T when trying to infer from SpecialBox to Box<T>, so inferring unknown is expected in that case.

RyanCavanaugh commented 2 hours ago

See https://github.com/Microsoft/TypeScript/wiki/FAQ#structural-vs-instantiation-based-inference

manbearwiz commented 31 minutes ago

I did stumble on that when investigating this, but I wasn't sure how that all worked. So if I'm understanding correctly,

Based on that, I tried a new box definition, const PlainBox = Box<Stuff>, and the inference does work with that.

class Box<T extends Record<string, unknown>> {
  // Uncomment the below to include a reference to T in the generic class and fix return type inference
  // private __uncommenting_fixes__?: T;

  // some fns that return T[K]
}

type Stuff = { foo: { a: number }; bar: { b: string }; };
class SpecialBox extends Box<Stuff> {}
const PlainBox = Box<Stuff>;

function unbox<
  Target extends Box<Record<string, unknown>>,
  K extends Target extends Box<infer Y> ? keyof Y : never,
  U extends Target extends Box<infer Z> ? Z[K] : never,
>(obj: Target, key: K): U {
  // todo
  return null as U;
}

// unbox<Box<Stuff>> and unbox<PlainBox> works fine, but unbox<SpecialBox> fails
console.log(unbox(new Box<Stuff>(), 'foo').a, unbox(new SpecialBox(), 'foo').a, unbox(new PlainBox(), 'foo').a)
console.log(unbox(new Box<Stuff>(), 'bar').b, unbox(new SpecialBox(), 'bar').b, unbox(new PlainBox(), 'bar').b)

TS Playground