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

Return type of inherited methods breaks function chaining #275

Closed
teamdandelion opened this issue Jul 28, 2014 · 10 comments
Closed

Return type of inherited methods breaks function chaining #275

teamdandelion opened this issue Jul 28, 2014 · 10 comments
Labels
Duplicate An existing issue was already created Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@teamdandelion
Copy link

Suppose I want to make a class with tons of chainable methods, like setters. This works fine.

class Chainable {
  public set1(arg) {
    this.arg1 = arg;
    return this;
  } 

  public set2(arg) {
    this.arg2 = arg;
    return this;
  }

  public set3(arg) {
    this.arg3 = arg;
    return this;
  }
}

This allows us to create and setup a new Chainable much more concisely than would otherwise be possible.

a = new Chainable().set1(foo).set2(bar).set3(baz);
// vs
a = new Chainable();
a.set1(foo);
a.set2(bar);
a.set3(baz);

The problem occurs when we want to extend our Chainable class and maintain the chainability of the function calls.

class SuperChainable extends Chainable {
  public set4(arg) {
    this.arg4 = arg;
    return this;
  }

  public set5(arg) {
    this.arg5 = arg;
    return this;
  }

  public set6(arg) {
    this.arg6 = arg;
    return this;
  }
}
a = new SuperChainable().set4(foo).set1(baz); 
// this is fine, because set4 returns a SuperChainable 

a = new SuperChainable().set1(baz).set4(foo);
// this breaks, because set1 is returning a Chainable, not SuperChainable

var a: SuperChainable;
a = new SuperChainable.set1(baz); // typeError, set1 returns a Chainable, not SuperChainable

There is a workaround, but it is verbose and boiler-platey:

class SuperChainable extends Chainable {
  public set4(arg) {
    this.arg4 = arg;
    return this;
  }

  public set5(arg) {
    this.arg5 = arg;
    return this;
  }

  public set6(arg) {
    this.arg6 = arg;
    return this;
  }

  public set1(arg) {
    super.set1(arg);
    return this;
  }

  public set2(arg) {
    super.set2(arg);
    return this;
  }

  public set3(arg) {
    super.set3(arg);
    return this;
  }
}

When set1 is called on a SuperChainable and returns this, the this really is a SuperChainable and not a Chainable. So it seems to me that the typescript compiler is in error in treating the return value as a Chainable. If this error were fixed, it would be much easier to create inherited objects that support method chaining.

(copied over from now-closed issue on codeplex: https://typescript.codeplex.com/workitem/2332)

@RyanCavanaugh
Copy link
Member

I believe the general idea here is that you should be able to say that a class method returns the type of whatever object it was invoked on; we generally called this "this-typing" in design discussions when it came up. It's certainly a useful and common pattern.

We'd like to see a proposal here (see https://github.com/Microsoft/TypeScript/wiki/Writing-Good-Design-Proposals) that defines syntax, type rules, etc.

@dsherret
Copy link
Contributor

If the base class is used like an abstract class and the child class is never inherited, then this is an ok workaround:

// http://stackoverflow.com/a/23024723/188246
class Base<T extends Base<any>> {
    promise() : T {
        return <any> this;
    }
}

class Child extends Base<Child> {
    public myString: string;
}

new Child().promise().myString; // works

@dsherret
Copy link
Contributor

I could maybe see this feature working if something like default type parameters or generic parameter overloads (#209) were implemented.

Rough example... very ugly though:

class Base<T extends Base<any> = Base> {
    promise() : T {
        return <any> this;
    }
}

class Child<T extends Child<any> = Child> extends Base<T> {
    public myString: string;
}

class ChildChild<T extends ChildChild<any> = ChildChild> extends Child<T> {
    public otherString: string;
}

new Child().promise().myString;
new ChildChild().promise().otherString;

@altano
Copy link

altano commented Jan 25, 2015

Hey everyone. Was there any progress on this in 1.4.1?

@DanielRosenwasser
Copy link
Member

This may now be a duplicate of #285.

@altano 1.4.1 does not have any changes regarding this

@joewashear007
Copy link

+1

@nolazybits
Copy link

Would love to get this one too. 👍

@dead-claudia
Copy link

Related: #3694

@dead-claudia
Copy link

As for a full proposal, I'd like to see how the community reacts to this vs the other bug first. A proposal would be better after that.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 16, 2015

this is tracked by #3694

@mhegazy mhegazy closed this as completed Sep 16, 2015
@mhegazy mhegazy added the Duplicate An existing issue was already created label Sep 16, 2015
@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created 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

9 participants