Skip to content

Return type of inherited methods breaks function chaining #275

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

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
# for free to subscribe to this conversation on GitHub. Already have an account? #.
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