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

Instances of generic interfaces are incompatible because their generic arguments are incompatible; even when two instances in question are structurally identical #61100

Open
fictitious opened this issue Feb 2, 2025 · 3 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@fictitious
Copy link

🔎 Search Terms

"type relationships" "type compatibility" instances generic interface "type parameter" measuring variance

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about type compatibility for generics

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.7.3#code/JYOwLgpgTgZghgYwgAgCoAtQHMCMyDeAUMicgG5wA2ArhAFzIDOYU2A3IQL6GGiSyIUGbACYCxUhRr0mLdhJIQAHizgNmrEFg7de4aPCTIA6lDgAHc9AA8wrcmWQQAE0YEptBtRABrEAHsAdxBOAD5xUnIqTzRMLQBtAHIPCESAXR0eZwgESjgoFAR-EGZkMDjcBlMLKyhbCpxQjiKSsDKKkSqzSxs7LBFwgF527BwOHjAATytkAHF-f2dqnrq+hxUIFzd8FK9fAOCw5GH8TmQAMgjJaJk+pJT0nXHs3PzC4tKsBec+nAZ5xbLWr1UZNQgtT7fPqdObfIG9DpDZBfRa-DhAA

💻 Code

interface Thing1 {
    value: string;
}

interface Thing2 {
    value: string;
    extra: string;
}

interface Wrapper<Thing extends {value: unknown}> {
    value: Thing['value'];
}

declare const thing1: Wrapper<Thing1>;
const thing2: Wrapper<Thing2> = thing1;

🙁 Actual behavior

Type 'Wrapper' is not assignable to type 'Wrapper'.
Property 'extra' is missing in type 'Thing1' but required in type 'Thing2'.

extra is not used at all in the definition of Wrapper, but somehow it affects the compatibility between different instantiations of Wrapper.

🙂 Expected behavior

No error, Wrapper<Thing1> and Wrapper<Thing2> are structurally identical and should ideally be the same type internally.

Additional information about the issue

The error does not happen if Wrapper is defined as an intersection with a dummy {} type:

type GoodWrapper<Thing extends {value: unknown}> = {} & {
    value: Thing['value'];
};

declare const goodThing1: GoodWrapper<Thing1>;
const goodThing2: GoodWrapper<Thing2> = goodThing1; // ok

The only similar issue that I managed to find is Generic interfaces flagged incompatible, as if nominal typing [fixed], which does not produce an unexpected error any more, but also is marked "Working as intended". The explanation is that it's a result of an optimization, and "This optimization depends on types being sensible".

If this is indeed a result of the optimization which is suppressed by using an intersection type, the question is how reliable that suppression is.

Also, I don't see anything not sensible in the example code - in the real code, types Thing1 and Thing2 are defined in the user code, and the Wrapper is defined in the library that only cares about one particular aspect of things.

@jcalz
Copy link
Contributor

jcalz commented Feb 2, 2025

I'm not a TS team member

Looks like #37052, TS decides that Wrapper<T> is covariant in T, so it doesn't want you to assign Wrapper<U> to Wrapper<T> unless you can assign U to T. That's not accurate because only the value property of T actually matters to the structure. TS uses variance measurements as a shortcut to make performance reasonable. A fallback to structural comparison would fix this, but it would be expensive. So it's probably a design limitation.

I don't know that I'd call the example "not sensible", but Wrapper<T> doesn't really care about T very much, which is weird. It essentially asks for a bunch of information it presumably just throws away. Instead I'd expect to see ActualWrapper<V> defined like

interface ActualWrapper<V> {
    value: V;
}

And then if you needed a shorthand to say ActualWrapper<T["value"]> you could define it:

type Wrapper<T extends { value: unknown }> = ActualWrapper<T["value"]>;

declare const thing1: Wrapper<Thing1>;
const thing2: Wrapper<Thing2> = thing1; // okay

@fictitious
Copy link
Author

fictitious commented Feb 2, 2025

It essentially asks for a bunch of information it presumably just throws away

That's because the code was simplified as much as possible, throwing away the parts not relevant to demonstrate the type relationships. The real code is not quite amenable to "your generics should use every piece of information available" rule.

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Feb 13, 2025
@RyanCavanaugh
Copy link
Member

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

I can see how you'd get into this state if you were using an object in lieu of many different type parameters? But it's not clear why you would have an extra property in that situation. A better motivating scenario would be helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

3 participants