Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incorrect type inference for generic types with 'prototype' property #55199

Closed
ApplY3D opened this issue Jul 29, 2023 · 7 comments
Closed

Incorrect type inference for generic types with 'prototype' property #55199

ApplY3D opened this issue Jul 29, 2023 · 7 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@ApplY3D
Copy link

ApplY3D commented Jul 29, 2023

Bug Report

🔎 Search Terms

  • generic type prototype inference

🕗 Version & Regression Information

  • This changed between versions 4.7.4 and 5.2.0
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about prototype

⏯ Playground Link

Playground link with relevant code

💻 Code

interface Type<T> {
    new(...args: any[]): T;
}
interface Proto<T> {
    prototype: T;
}

function injectWithType<T>(a: Type<T>): T { return null as T };
{
    const okUnknown = injectWithType(Set); // Set<unknown> ⚠️
    const okNumber = injectWithType<Set<number>>(Set); // Set<number> ✅
    const okNumber2 = injectWithType(Set<number>); // Set<number> ✅
}

function injectWithProto<T>(a: Proto<T>): T { return null as T };
{
    const okAny = injectWithProto(Set); // Set<any> ⚠️
    const okNumber = injectWithProto<Set<number>>(Set); // Set<number> ✅
    const unexpectedAny = injectWithProto(Set<number>); // Set<any> ‼️
}

The compiled version has a bug too - double parentheses (reproduced both with the injectWithType and the injectWithProto.):

const unexpectedAny = injectWithProto((Set)) // >= [email protected]
const unexpectedAny = injectWithProto(Set()) // < [email protected]

🙁 Actual behavior

When using an interface or atype with the prototype property to declare class, generic type is getting wrong, especially in the ExpressionWithTypeArguments (‼️) case.

🙂 Expected behavior

(⚠️) - These lines should have the same type (Set<any> or Set<unknown>)
(‼️) - This line should correctly infer generic type (Set<number>)

const arrOfNum = injectWithProto(Set<number>) // should be Set<number>, not Set<any>
@jcalz
Copy link
Contributor

jcalz commented Jul 30, 2023

There's a lot going on here, could you reduce to a single bug report?

The stuff about the emitted JavaScript seems like a separate topic entirely... and I don't see how that's supposed to be a bug either; injectWithProto((Set)) is weird but does it do something incorrectly somewhere? And since instantiation expressions (#47607) were introduced in TS4.7, I wouldn't have any expectations at all for what JS is emitted if you try to use them on older versions. Maybe you could remove that part and be explicit about what specific single bug you're reporting here?

Are you saying that the type of Set.prototype should be Set<unknown> instead of Set<any>? Are you saying that the type of (Set<number>).prototype should be Set<number> instead of Set<any>? Which one of those is the bug you're reporting?

@ApplY3D
Copy link
Author

ApplY3D commented Jul 30, 2023

@jcalz I'm reporting about type issue mainly. Both result types below should be equal Set<number>, not only the first one:

injectWithProto<Set<number>>(Set)
injectWithProto(Set<number>);

The compiled version is not breaking, but it's weird and might be related to type inference issue I'm repporting.

@jcalz
Copy link
Contributor

jcalz commented Jul 30, 2023

It's unrelated, so having it here is distracting at best. It also seems like you don't care primarily about Set<unknown> vs Set<any>, so maybe that's also just distracting.

If I were filing this issue it would be as a suggestion (not a bug report) that (Set<number>).prototype be of type Set<number>, or more generally that using an instantiation expression on a class constructor would make the prototype property have the same type as its instance type.

The typing of prototype is weird in TS anyway; we pretend that a class prototype has the same type as an instance, even though this is very likely untrue for any class with instance properties. And if you have a generic class Foo<T> then it has a single prototype Foo.prototype which would therefore need to be the the prototype for all possible instantiations of T, like an infinite intersection Foo<A> & Foo<B> & Foo<C> & ⋯, or forall T. Foo<T>... and Foo<any> is really the closest thing we have to that. So to some extent we can't really rely on prototype to have a fully accurate type.

But since class Bar extends Foo<X> {} has Bar.prototype of type Bar, it's not unreasonable to expect that (Foo<X>).prototype would be of type X also. I'd still be phrasing this as a feature request though.

@ApplY3D
Copy link
Author

ApplY3D commented Jul 30, 2023

@jcalz The main issue is that I expect the same behavior for both function calls injectWithProto, but they are completely different. I care only about this.

...Both result types below should be equal Set<number>, not only the first one:

injectWithProto<Set<number>>(Set)
injectWithProto(Set<number>);

@RyanCavanaugh
Copy link
Member

I'm confused too. Different lines of code may well have different behavior; that's how stuff gets done. Which line is wrong, and why? Can you post a straightforward example with no other things going on?

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Jul 31, 2023
@ApplY3D
Copy link
Author

ApplY3D commented Aug 1, 2023

@jcalz oh, I understood... SetConstructor.prototype is Set<any>, maybe it really should be a suggestion as you wrote.
@RyanCavanaugh, the real world example how I catched this bug, but simplified. There are 2 files.

@RyanCavanaugh
Copy link
Member

This isn't an actionable description of a bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

3 participants