Skip to content

(suggestion) Overrides in extending class and interface definitions. #3402

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
ttowncompiled opened this issue Jun 6, 2015 · 10 comments
Closed
Labels
By Design Deprecated - use "Working as Intended" or "Design Limitation" instead Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@ttowncompiled
Copy link

Using this example:

interface A {
  build(): SomeType;
}

interface B extends A{
  build(): SomeOtherType;
}

The compiler will thrown an error that SomeOtherType is not assignable to SomeType. However, if I am holding an object of type B and I call build() on that object, I know that I will receive a value of type SomeOtherType.

I've currently used any as an override, but it would nice to be able to override the return type of functions on a parent class or interface.

@DanielRosenwasser
Copy link
Member

But then B wouldn't actually be a subtype of A. It sounds like you just want code reuse for other inherited methods. Couldn't you just refactor your logic from A out to another class that A and B both extend from, and have each provide their own build method?

@ttowncompiled
Copy link
Author

Very true, and that's usually the first route I would take. I've been hoping to avoid that though. Lately, my free time has been spent writing type definitions for DefinitelyTyped. Since the projects I'm defining interfaces for aren't my own, I was hoping to keep the types as strict to the project as possible.

That aside, the reason I wanted to suggest this was to point out an example where a more JS approach to objects and inheritance had value. Having types and laying strict constraints on inheritance is really nice, especially for someone who grew up in Java. However, the design philosophy for JS is more free form and there are a few liberties that it provides that become missed when I more strict type design is taken.

On that note though, I do want to say that I'm mostly anxious to suggest ideas because I think TypeScript is awesome and I really appreciate the time that all of you take to post back to suggestions like these, even when it's just an anxious young programmer who often times comes off like he can't be wrong.

@danquirk
Copy link
Member

danquirk commented Jun 8, 2015

However, if I am holding an object of type B and I call build() on that object, I know that I will receive a value of type SomeOtherType.

But when you are holding an object of type B through a value of type A then what?

interface A {
  build(): number;
}

interface B extends A{
  build(): string;
}

var a: A;
var b: B;
a = b;
var result = a.build();
var aNumberOrACrash = result.toFixed()

The point of interfaces is really to verify contracts and prevent situations like this. It sounds like you're more interested in using interfaces to mix in existing functionality for code re-use (although ideally you'd also be able to mix in an actual implementation). We've previously held off on this kind of feature because it has been proposed for ES7 (or later) and we're hesitant to implement things which may later conflict with the ES standard. There isn't really a canonical thread for discussion on there but if you search the issue tracker for 'mixin' you'll find a few different threads where we've talked about options: https://github.com/Microsoft/TypeScript/search?q=mixin&type=Issues&utf8=%E2%9C%93

Thanks for the feedback though, keep it coming :)

@danquirk danquirk closed this as completed Jun 8, 2015
@danquirk danquirk added Suggestion An idea for TypeScript Declined The issue was declined as something which matches the TypeScript vision By Design Deprecated - use "Working as Intended" or "Design Limitation" instead labels Jun 8, 2015
@ttowncompiled
Copy link
Author

Thank you for the info.

@aleung
Copy link

aleung commented Feb 15, 2016

Wrapper module usually just override one (or some) method of the original module. How should I write .d.ts for it if not to modify the .d.ts file of the original module?

@mredbishop
Copy link

We would find it really useful to be able to do this also. In a number of places we are using a third party typing for some interface that declares one of it's sub properties or methods as as type any and we would like to create more strongly typed interfaces based on these. For example the angular.ui.IStateService interface has a property called current which in turn has a property called data for storing arbitrary ....wait for it ... data (shocking I know). It would be great if we could create typings that more strongly type the data property so that we have typing in the state controller. We could get the same effect by declaring an object of a custom type and setting it to the value of $state.current.data but this is more code.

EDIT
I think I've changed my mind, I agree that it would be much better implemented as mix ins of some kind.

@tarruda
Copy link

tarruda commented Nov 22, 2016

But when you are holding an object of type B through a value of type A then what?

@ttowncompiled why not throw a compiler error if an overload has the same parameters with different return type? I think this is what C++/Java compilers do, because overload is only possible for different types/number of parameters(otherwise it would be impossible for the compiler to determine which method to call).

To argument in favor of implementing this feature, I will describe a use case of some real world libraries that would benefit from overloading in subinterface: levelup and its extension sublevel.

Levelup batch method first argument is an object with this interface:

interface Batch {
    type: string;
    key: any;
    value?: any;
    keyEncoding?: string;
    valueEncoding?: string;
}

The sublevel library usage is something like this:

import levelup = require('levelup');
import sublevel = require('level-sublevel');

var db = sublevel(levelup('/path/to/db'));

The db interface is similar to the one returned by the levelup function, but the first argument to the batch method can also contain an additional prefix field. At the moment, this is the DefinitelyTyped declaration file for sublevel:

/// <reference types="levelup" />

interface Hook {
    (ch: any, add: (op: Batch|boolean) => void): void;
}

interface Batch {
    prefix?: Sublevel;
}

interface Sublevel extends LevelUp {
    sublevel(key: string): Sublevel;
    pre(hook: Hook): Function;
}

declare module "level-sublevel" {
    function sublevel(levelup: LevelUp): Sublevel;
    export = sublevel;		
}

The interesting part is that the Sublevel interface extends LevelUp to contain a couple of additional methods, but the Batch interface used in LevelUp is augmented with an additional property instead of being extended, which would better represent the relationship. Ideally it would look like this:

interface SubBatch extends Batch {
    prefix?: Sublevel;
}

But since it is not possible for Sublevel to overload the batch method to accept a SubBatch argument, the declaration file has to augment the existing type.

The problem with this approach is that the LevelUp.batch interface also accepts the augmented Batch interface, which only makes sense in the context of Sublevel.

@caesay
Copy link

caesay commented May 15, 2017

@danquirk I think this was prematurely declined. Here's an example of where this might be extremely useful:

import { SomeComponent, SomeComponentProps } from "./SomeComponent";

interface BlahProps {
    // SomeComponentProps has an onChange method with a different signature.
    onChange: (someTest: string) => void,
}

class Blah extends React.Component<BlahProps & SomeComponentProps, any> {

    handleChildChange = (e: any) => {
        // do something with the event child event
    }

    render() {
        let { onChange, ...other } = this.props;
        return <SomeComponent onChange={this.handleChildChange} {...other}  />;
    }
}

Whether you use a union type or extend the type, the onChange property becomes totally unusable in this case, so the only option is to leave onChange ignored, and create a different named property like onCustomChange or something.

There will never be anything accessing through the base type SomeComponentProps, it's purely there to allow you to pass properties through to a child component, and if you override a property then its probably thought through and dealt with accordingly.

Perhaps the best solution could be a way to create a new type that omits some properties from an existing interface?

interface A {
    t1: string;
    t2: number;
    other: object;
}

type B = A without t1, other;

interface C extends B {
    t3: object;
}

// result: C has only t2, t3.

@RyanCavanaugh
Copy link
Member

@caesay see #4183 for that

@gund
Copy link

gund commented May 2, 2018

If you really want to modify a property on some interface you can workaround by producing intermediate type that will weaken desired property to be modified:

export interface A {
  prop1: string;
  prop2: number;
}

// This is intermediate type and can be kept private
interface AWeak extends A {
  prop1: any
}

export interface ANew extends AWeak {
  prop1: boolean; // No errors here as `prop1` is already of type `any`
}

EDIT: I just played a little bit with this and created a mapped type that will allow you to skip extra type creation in order to override desired prop, I called it Weaken because that's what it does - makes specified properties as any:

type Weaken<T, K extends keyof T> = {
  [P in keyof T]: P extends K ? any : T[P];
};

And then you can simply:

export interface A {
  prop1: string;
  prop2: number;
}

export interface ANew extends Weaken<A, 'prop1'> {
  prop1: boolean; // No errors here as `prop1` is weakened to type `any`
}

What do you think should this helper type be added to Typescript's default definitions library?

@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
By Design Deprecated - use "Working as Intended" or "Design Limitation" instead Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

9 participants