Skip to content

TS Proposal : "Interface incorrectly extends interface" - sub-interface method overload OR override ? #20920

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

Open
SalathielGenese opened this issue Dec 28, 2017 · 6 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@SalathielGenese
Copy link

SalathielGenese commented Dec 28, 2017

TypeScript Version : 2.6.2

The problem

Initial scenario

I encountered the issue yesterday days as I was trying to add some features to one of my interface inheritance chain. The context of this example is just imagined : It is longer than needed to demonstrate the issue, but I wanted a full example to clearly show it needs to be addressed.

So, let's imagine a testing app that should instrument devices : the following is a potential model of the situation :

interface Stream
{
}
interface InputStream
{
}
interface OutputStream
{
}
// interface inheraitance is a great feature
interface IOStream extends InputStream, OutputStream
{
}
interface InterfaceLike
{
}
interface DeviceInterfaceLike extends InterfaceLike
{
    init();
}
interface InputDeviceInterfaceLike extends DeviceInterfaceLike
{
    input(); // this should word just like the `touch` command
             //     like if there's and error with the device, it will trigger exception
    input(stream: InputStream);
}
interface ScreenDeviceInterfaceLike extends InputDeviceInterfaceLike
{
    input(stream: InputStream, coordinates: {x: number, y: number});
}
interface TouchScreenDeviceInterfaceLike extends InputDeviceInterfaceLike
{
    input(stream: IOStream, coordinates: {x: number, y: number});
}

Expected behavior :

It should just overload that method in the sub-interfaces.

Actual behavior :

error TS2430: Interface 'ScreenDeviceInterfaceLike' incorrectly extends interface 'InputDeviceInterfaceLike'.
  Types of property 'input' are incompatible.
    Type '(stream: InputStream, coordinates: { x: number; y: number; }) => any' is not assignable to type '{ (): any; (stream: InputStream): any; }'.
core.ts(73,11): error TS2430: Interface 'TouchScreenDeviceInterfaceLike' incorrectly extends interface 'InputDeviceInterfaceLike'.
  Types of property 'input' are incompatible.
    Type '(stream: IOStream, coordinates: { x: number; y: number; }) => any' is not assignable to type '{ (): any; (stream: InputStream): any; }'.
18:38:26 - Compilation complete. Watching for file changes.

It requires me to fully copy paste all method signatures in each interface.

interface ScreenDeviceInterfaceLike extends InputDeviceInterfaceLike
{
    input(); // this should word just like the `touch` command
             //     like if there's and error with the device, it will trigger exception
    input(stream: InputStream);
    input(stream: InputStream, coordinates: {x: number, y: number});
}
interface TouchScreenDeviceInterfaceLike extends InputDeviceInterfaceLike
{
    input(); // this should word just like the `touch` command
             //     like if there's and error with the device, it will trigger exception
    input(stream: InputStream);
    input(stream: IOStream, coordinates: {x: number, y: number});
} 

When you're can have up to (why not) 15 overloads distributed through inheritance chain... It becomes, (yes, you said it) bulky.


Now it is, that the compiler cannot yet figure out when to override the method signatures inherited from parents and when to overload them.

The Proposal

My first thought was to introduce a new keyword just like @ts-nocheck and like.

@override

interface InputDeviceInterfaceLike extends DeviceInterfaceLike
{
    @override // to override all definitions from parents
    init(istream: InputStream);
}

OR

interface InputDeviceInterfaceLike extends DeviceInterfaceLike
{
    @override
    {
        init(); // to override only this (these) definitions : forget all previous definition but this (these)
    }
    init(istream: InputStream);
}

Then, the TouchScreenDeviceInterfaceLike interface would become :

interface TouchScreenDeviceInterfaceLike extends InputDeviceInterfaceLike
{
    @override   // it is now clear that the only way to instrument a touchscreen is to provide a `IOStream` and coordinates (`{x: number, y: number}`)
    input(stream: IOStream, coordinates: {x: number, y: number});
}

Furthermore, @overload

While the previous @override is useful when the default behavior is oveloading, we can still define the default behavior to suppress all parent definition WHEN A METHOD IS REDEFINED in subinterface, except when @overload is used. i.e

interface InputDeviceInterfaceLike extends DeviceInterfaceLike
{
    // to override all definitions from parents
    init(istream: InputStream);
}

OR

interface InputDeviceInterfaceLike extends DeviceInterfaceLike
{
    @overload // to overload signatures from parents
    init(istream: InputStream);
}

And TouchScreenDeviceInterfaceLike interface can become :

interface TouchScreenDeviceInterfaceLike extends InputDeviceInterfaceLike
{
    @overload                           // to overload signatures from parents
    {
        init(istream: InputStream);     //      except this signature... So that it remains only the empty parameter and the below
    }
    input(stream: IOStream, coordinates: {x: number, y: number});
}

By thinking rigorously, I think the first step would be allow inheriting type to overload methods by default.

= = = = = = UPDATE = = = = = =
I now think it worth to add that by no mean, all through an interface inheritance chain should a method be completely wiped off (deleted, removed)... Not even by @Override! As this may (will) break the core concept of OOP inheritance.

The mechanism is to ensure that only some methods mood (signatures) are handled (accepted) at a given inheritance node.

In other words, if @Override suppresses all signatures, at least one method signature MUST be required.

@SalathielGenese SalathielGenese changed the title Interface 'ScreenInputDeviceInterfaceLike' incorrectly extends interface 'InputDeviceInterfaceLike' TS Proposal : "Interface incorrectly extends interface" - sub-interface method overload OR override ? Dec 30, 2017
@mhegazy mhegazy added the Suggestion An idea for TypeScript label Jan 4, 2018
@shobhitg
Copy link

shobhitg commented Feb 1, 2018

This issue is also applicable for enums that are overridden.

interface BaseType
{
    allowedSounds: 'BARK' | 'MOO';
}
interface DerivedType extends BaseType
{
    allowedSounds: 'BARK' | 'MOO' | 'CAW';
}

This currently results in the following error:

Interface 'DerivedType' incorrectly extends interface 'BaseType'.
  Types of property 'allowedSounds' are incompatible.
    Type '"BARK" | "MOO" | "CAW"' is not assignable to type '"BARK" | "MOO"'.
      Type '"CAW"' is not assignable to type '"BARK" | "MOO"'.

But from a language point of view, it is a totally legit expectation to be able to override enums in DerivedTypes.

@RyanCavanaugh
Copy link
Member

But from a language point of view, it is a totally legit expectation to be able to override enums in DerivedTypes.

This is a correct error. DerivedType is not a BaseType because a piece of code looking at a DerivedType through a BaseType reference does not expect to see the value CAW.

@shobhitg
Copy link

shobhitg commented Feb 5, 2018

@RyanCavanaugh Actually on second thought, I agree with you. I myself was feeling conflicted about it, but wrote that out anyway.

@SalathielGenese
Copy link
Author

SalathielGenese commented Feb 5, 2018

I now think it worth to add that by no mean, all through an interface inheritance chain should a method be completely wiped off (deleted, removed)... Not even by @Override! As this may (will) break the core concept of OOP inheritance.

The mechanism is to ensure that only some methods mood (signatures) are handled (accepted) at a given inheritance node.

In other words, if @Override suppresses all signatures, at least one method signature MUST be required.

@SalathielGenese
Copy link
Author

SalathielGenese commented Feb 6, 2018

As I think over it... I realize that this will conflict with some end-user decorator named Override. While any, keyof and some other strings are being reserved as TypeScript keyword... I suggest and alternative.

@!Override or just !Override(why not)

This is non conflicting as it is currently a syntax error.

@RyanCavanaugh RyanCavanaugh added the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Feb 6, 2018
@SephReed
Copy link

SephReed commented Jul 20, 2018

I've got some react props along the lines of:
IValidationTextAreaProps extends IAutoSizeTextAreaProps extends HTMLProperties<HTMLTextArea>

It's really annoying having to use three different names for the onChange listener.

HTMLTextarea has onChange: (event) => boolean
AutoSizeTextArea has onValueChange: (newVal) => void
and I'm having to come up with yet another contrived name for ValidationTextAreaProps... probably
valueChangeListener: (newVal, isValid) => void
and hopefully nobody tries using onChange or onValueChange.

As a proposal:

Remove it from typscript compiler entirely and just make it a linting error

// ts-lint:disable:no-interface-overrides
ISomeInterface extends { name: string} {
  name: string[],
}

or if this seems too crazy, these might work:

ISomeInterface extends { name: string} {
  !name: string[],
}

or

ISomeInterface extends { name: string} {
  name: string[] !allow-override,
}

anything really, as long as typescript isn't blocking this js functionality.

@zhengxiaoyao0716
Copy link

zhengxiaoyao0716 commented May 29, 2019

Maybe we can just using an custom type to resolve it:

type ProtoExntends<T, U> = U & {
  [P in Exclude<keyof T, keyof U>]: T[P];
};

Well, I found that there was an easier way used the builtin types:

type ProtoExntends<T, U> = U & Omit<T, keyof U>;

And for the example usage:

interface Parent {
  aaa: number;
  bbb: number;
  // .
  input(stream: {}): void;
  allowedSounds: 'BARK' | 'MOO';
}
interface ChildExtends {
  aaa: string;
  ccc: string;
  // .
  input(stream: {}, coordinates: {x: number; y: number}): void;
  allowedSounds: 'BARK' | 'MOO' | 'CAW';
}
type Child = ProtoExntends<Parent, ChildExtends>;

It looks everything works well as the screenshots shows:
image
image
image
image
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants