Skip to content

Method overloads are not inherited from implemented interfaces #56909

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

Closed
jeengbe opened this issue Dec 31, 2023 · 9 comments
Closed

Method overloads are not inherited from implemented interfaces #56909

jeengbe opened this issue Dec 31, 2023 · 9 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@jeengbe
Copy link

jeengbe commented Dec 31, 2023

πŸ”Ž Search Terms

method function overload interface intheritance

πŸ•— Version & Regression Information

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

⏯ Playground Link

https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgJLIN4Chm+TEACgGcwoAuZAIjioEpKA3Ae2ABMBuHPAksyqgCN6TVpywBfLFgQAbOMWLIAwsmABbAA6yI6iOCXpsefEVIVk50AHMGyFu0xSpWNhDlwoKBMxClkCJTKXDIAdARYAPSRyAB6APxRMQDKABbMAK6ybMiCKHwWNCL2YsgA7sBgqcgA1ACMyMyM0LLMcGxAA

πŸ’» Code

interface I {
    fn(str: "a"): void;
    fn(str: "b"): void;
}

class C implements I {
    fn(str: string): void {}
}

declare const c: C;

c.fn
// ^?
// Should be (str: "a"): void with +1 overload

πŸ™ Actual behavior

Both signatures for fn declared in I should be inherited by C, and the generic implementation that takes str: string should not be considered the primary signature. Instead, the signatures fn(str: "a"): void and fn(str: "b"): void should come from the interface.

This would also make sense when implementing several interfaces with different signatures for the same function, you wouldn't only want to get the merged function in the end there.

πŸ™‚ Expected behavior

see above.

Additional information about the issue

No response

@jeengbe
Copy link
Author

jeengbe commented Dec 31, 2023

The primary question is whether this is even intended - does it make sense to discard the class' signature in favour of ones defined in any interfaces? I can't think of situations from the top of my head where this would not be the case.

@MartinJohns
Copy link
Contributor

MartinJohns commented Dec 31, 2023

It's intended. Your class method can deal with any string, why throw that information away? And if the behavior you describe would be the case, how would one write a class method that is publicly accepting any string? Just because the class implements the interface doesn't mean it's only used via the interface.

@jeengbe
Copy link
Author

jeengbe commented Dec 31, 2023

Your class method can deal with any string, why throw that information away?

The same argument applies to regular method overloads, fn can deal with any string here too, but only the overloads are considered.

class C {
    fn(str: "a"): void;
    fn(str: "b"): void;

    fn(str: string): void {}
}

declare const c: C;

c.fn
// ^?
// Now correctly is (str: "a"): void with +1 overload

It's intended. Your class method can deal with any string, why throw that information away? And if the behavior you describe would be the case, how would one write a class method that is publicly accepting any string? Just because the class implements the interface doesn't mean it's only used via the interface.

That is the one major problem I see with this too, see my previous comment are my thoughts on that.

@MartinJohns
Copy link
Contributor

The same argument applies to regular method overloads, fn can deal with any string here too, but only the overloads are considered.

Because in that case you explicitly declare it as such. The public API explicit declares only those two values are supported. You don't do that in your initial example, you don't provide any overloads in your class.

Perhaps it helps you to know that implemented interfaces in TypeScript are more of an assignability check, making sure your class is assignable to the interface.

@jeengbe
Copy link
Author

jeengbe commented Dec 31, 2023

That makes sense, but doesn't quite align with the fact that JSDoc is inherited from implemented interfaces:

interface I {
    /**
     * Does a thing
     */
    fn(): void;
}

class C implements I {
    fn(): void {}
}

declare const c: C;

c.fn

I understand that's a different thing, but still, it displays some kind of "inheritance".

@fatcerberus
Copy link

This really is no different from asking why...

interface I {
    fn(str: "a"): void;
    fn(str: "b"): void;
}

class C implements I {
    fn(str: string): void {}
    fn2(num: number): void {}
}

declare const c: C;

c.fn2(42);

...works. The contract implied by the interface is met (fn can accept "a" or "b"), the fact that you can pass other strings to fn, from the point of view of the type system, is just added functionality on top of that. (note also that objects can implement multiple interfaces, so it makes no sense to limit the implementation to only what's in the interface)

Ultimately, I think this is, if not an exact duplicate, heavily related to #32082.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jan 2, 2024
@RyanCavanaugh
Copy link
Member

The intentional design here is that writing an implements clause doesn't cause your class type to become less useful, which I think makes a lot of sense if you're thinking about the behavior holistically. It wouldn't make sense to have class C { ... and your code works, then you add class C implements I1 and suddenly you get an error somewhere - implements shouldn't be able to cause your class to lose capability.

@jeengbe jeengbe closed this as not planned Won't fix, can't repro, duplicate, stale Jan 2, 2024
@fatcerberus
Copy link

It wouldn't make sense to have class C { ... and your code works, then you add class C implements I1 and suddenly you get an error somewhere

Hmm, I’m struggling to figure out how this could be worded better but coming up empty. The hypothetical error this would cause would be in the class definition itself, and clearly adding an implements clause can cause new errors there if the class doesn’t actually satisfy the interface, so putting it this way just seems like begging the question - we’ve come full circle back to the question of what exactly it means to satisfy an interface.

@RyanCavanaugh
Copy link
Member

What I mean is that elsewhere if you had

const x = new C();
x.fn("hello world");

then you shouldn't get a new error on "hello world" by adding an implements to C.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants