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

Suggestion - Alternate constructor syntax #1970

Closed
JeroMiya opened this issue Feb 6, 2015 · 3 comments
Closed

Suggestion - Alternate constructor syntax #1970

JeroMiya opened this issue Feb 6, 2015 · 3 comments
Labels
Duplicate An existing issue was already created

Comments

@JeroMiya
Copy link

JeroMiya commented Feb 6, 2015

Motivation

It's often the case when using dependency injection in JavaScript, to want to close over class constructor arguments in instance methods, rather than assign them to private fields and access them via 'this'. Here is an example of an AngularJS controller that does this:

function MyController($scope /* plus 10 or so others, sometimes 15, in a real application */) {
  this.canSave = function() {
    return !$scope.myForm.$dirty && !$scope.myForm.$error;
  };
}
MyController.$inject = ['$scope'];

Here is this controller expressed as a TypeScript class:

class MyController {
  canSave: () => boolean;
  static $inject = ['$scope'];
  constructor($scope: ng.IScope) {
    this.canSave = function() {
      return !$scope.myForm.$dirty && !$scope.myForm.$error;
    };
  }
}

As you can see, it's a lot more ceremony than is necessary. You can clean it up only by making canSave a prototype method, and assigning $scope and all your other dependencies as private fields:

class MyController {
    static $inject = ['$scope'];
    constructor(private $scope: ng.IScope) {}
    canSave() { return !this.$scope.myForm.$dirty && !this.$scope.myForm.$error; }
}

But this is not equivalent to the first example in javascript. You're forced to create private variables for your injected constructor arguments. What I'd like to do is use the same light-weight constructor argument syntax as other languages like Scala, F#, and C# 6, where the arguments are next to the class name:

Proposed new syntax

// NEW syntax:
class MyController($scope: ng.IScope) {
  canSave = () => !$scope.myForm.$dirty && !$scope.myForm.$error;
}

Of course, if you wanted to still use prototype functions instead of instance functions, you still could:

class MyController(private $scope: ng.IScope) {
  canSave() { return !this.$scope.myForm.$dirty && !this.$scope.myForm.$error; }
}

And, if you still need to run code in the constructor, re-use the constructor syntax but omit the argument list:

class MyController(private $scope: ng.IScope, assignGuid: boolean) {
  guid: string;
  constructor {
    this.guid = assignGuid ? GuidLib.newGuid() : null;
  }
}

This syntax makes it really nice to define small DTO-like object classes with an absolute minimum of ceremony:

    class Node {}
    class BinaryOperator(public name: string) extends Node {}
    class AdditionOperator extends BinaryOperator
      constructor { super('+'); }
    }
    // even less ceremony, potentially:
    class SubtractionOperator extends BinaryOperator('-') {}

Prior Art

As mentioned above, this syntax is available in a number of other excellent languages. Here are three:

Scala

Scala eliminates the constructor function entirely. The class definition itself is the constructor:

class Greeter(message: String) {
    println("This code is in the constructor.")
    def SayHi() = println(message)
}

Scala can also define members inline in the constructor argument list, just like TypeScript:

class Greeter(val message: String) {
  def sayHi() = println(this.message) // println(message) also works
}

F#

F# is similar to Scala. do bindings must occur before member definitions but can appear before or after let bindings. I'm not sure if it allows inline definition of members in the argument list. Apologies if the syntax is a little off, my F# isn't that great:

type Greeter(message: string) =
  do printfn "This code is in the constructor."
  let field = "foo"
  member this.SayHi = printfn message

C# 6

C# 6 has a primary constructor syntax. An empty block defines the constructor body. However you can't define members directly from the argument list:

class Greeter(string message) {
  {
    Console.WriteLine("This code is in the constructor. message was: " + message);
  }
  public string Message { get; } = message; // because magic? hidden instance field?
  public void SayHi() {
    // I'm not exactly sure how member functions gain access to constructor arguments
    Console.WriteLine("message: " + message);
  }
}

My proposal is most heavily influenced by Scala and C# 6. Like Scala, you can define members directly from the argument list (TypeScript could already do this), but like C# 6 and unlike Scala, the primary constructor body code must be contained within a single code block instead of just any code in the class definition block. In C# this is just an empty block, but in my proposed syntax, we retain the 'constructor' keyword preceeding the constructor body, but without arguments.

You could potentially get closer to Scala's constructor body syntax (which eliminates the extra syntax ceremony of a constructor block). I'm not sure if you'd want to (or if you could). But, if we did, I'd take inspiration from F#'s ordering requirements and ensure all constructor body code to appear before the first prototype member definition:

// a probably controversial alternative, similar to F# do bindings.
class MyController($scope: ng.IScope, $log: ng.ILogService) {
    $log.info('MyController was instantiated!');
    field = $scope.myForm; // yeah... this is probably ambiguous syntax. assigning a global or definining an instance field? That's why I went with the constructor block syntax instead.
    $log.info('initial value of myForm.$dirty: ' + $scope.myForm.$dirty.toString());
    sayHi() { alert('hello!'); }
    // ERROR: primary constructor statements must appear before the first prototype member definition
    // $log.info('can't do this');
}
@danquirk
Copy link
Member

danquirk commented Feb 7, 2015

I think my comment here mostly covers the issues with this #479 (comment)

@JeroMiya
Copy link
Author

JeroMiya commented Feb 7, 2015

It's a fair point, but I think the risk is relatively low. It's difficult to imagine this syntax being adopted as an ES standard. First, there is already a constructor syntax, so some would view it as redundant. Secondly, unlike typescript, regular JS doesn't need to declare fields first before assigning them in the constructor.

In ES, I would just write this, which isn't too bad:

class MyClass { constructor($scope) {
  this.can save = () => $scope.$dirty;
}}

Also, I don't think adding default parameters to the constructor argument list is a sufficient or desirable workaround. It changes the signature of the constructor which may have architectural side effects.

@danquirk
Copy link
Member

danquirk commented Feb 7, 2015

It may be that the risk is relatively low, but then the upside is relatively low (slightly more convenient syntax, no change in expressivity) while the downside is potentially high (breaking old TS code to maintain compatibility with some future ES version). I definitely get the desire for features like this and am personally accustomed to this one in particular from other languages I use but we do have to be judicious with expression level syntax additions.

@danquirk danquirk closed this as completed Feb 7, 2015
@danquirk danquirk added the Duplicate An existing issue was already created label Feb 7, 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
Projects
None yet
Development

No branches or pull requests

2 participants