A way to allow arbitrary properties on interfaces/classes

Topics: Language Specification
Oct 3, 2012 at 1:32 AM

Although having static typing is awesome and great and I love it, sometimes it would be nice to have dynamic typing as well, in a "having my cake and eating it too" kind of way.

Concrete example: I've made a start on a Declaration Source File for AngularJS (http://github.com/markrendle/AngularTS). An Angular controller in plain JS looks like this:

function HomeController($scope) {
    $scope.title = "AngularTS";
}

That can be declared as a class with a constructor in TypeScript, works great. The issue is that the $scope argument is an instance of the Angular class Scope, which has methods and properties for which I am declaring a TypeScript class, but it is also valid to set arbitrary properties on the $scope instance, as in the above code.

Could there be a modifier or a special member or something which would allow a class or interface to accept arbitrary properties in this way, so that this would work:

class HomeController {
    constructor ($scope: Scope) {
        $scope.title = "AngularTS";
    }
}

Suggestions:

// Add "implements dynamic" to a class or interface
declare class Scope implements dynamic {
    $destroy();
    $digest();
}

// Add a "dynamic" modifier to class or interface declarations
declare dynamic class Scope {
    $destroy();
    $digest();
}

// Add a special member type, e.g. ".*":
declare class Scope {
    $destroy();
    $digest();
    .*: any;
}
Coordinator
Oct 5, 2012 at 9:31 PM

While classes in TypeScript aren't open-ended, interfaces are.  The Warship sample does this, where it uses jquery.d.ts to describe interfaces which are later added to by jqueryui.d.ts. 

By modeling it using interfaces, you could extend the interface further as you extended the object.

Oct 7, 2012 at 12:36 AM

I was just looking at a similar issue. According to the language specification, a Declaration Source File (*.d.ts) is restricted to ambient declarations. According to the language specification, ambient declarations can be for variables, functions, classes and modules. Is this just missing information from the language specification?

Oct 7, 2012 at 9:47 AM

So the expected way to do this would be something like:

// in angular.d.ts
interface Scope {
    $digest(): void;
}

// in HomeController.ts
interface HomeScope extends Scope {
    pageTitle: string;
}

class HomeController {
    constructor($scope: HomeScope) {
        $scope.pageTitle = "Home";
        $scope.$digest();
    }
}

Is that right?

Coordinator
Oct 8, 2012 at 7:26 PM

In addition to your example, you can also reuse Scope and extend the interface.  It's up to the situation which one works better.  Here's an example of extending:

// in angular.d.ts
interface Scope {
    $digest(): void;
}

// in HomeController.ts
interface Scope {
    pageTitle: string;
}

class HomeController {
    constructor($scope: Scope) {
        $scope.pageTitle = "Home";
        $scope.$digest();
    }
}
Oct 8, 2012 at 8:06 PM

Yes, I saw that that would work too, but having a custom Scope interface per controller seems to be a better pattern for Angular.

Cheers,
Mark

On Oct 8, 2012 7:26 PM, "jonturner" <notifications@codeplex.com> wrote:

From: jonturner

In addition to your example, you can also reuse Scope and extend the interface. It's up to the situation which one works better. Here's an example of extending:

// in angular.d.ts
interface Scope {
    $digest(): void;
}

// in HomeController.ts
interface Scope {
    pageTitle: string;
}

class HomeController {
    constructor($scope: Scope) {
        $scope.pageTitle = "Home";
        $scope.$digest();
    }
}

Read the full discussion online.

To add a post to this discussion, reply to this email (typescript@discussions.codeplex.com)

To start a new discussion for this project, email typescript@discussions.codeplex.com

You are receiving this email because you subscribed to this discussion on CodePlex. You can unsubscribe or change your settings on codePlex.com.

Please note: Images and attachments will be removed from emails. Any posts to this discussion will also be available online at codeplex.com

Dec 20, 2012 at 2:32 PM
Edited Dec 20, 2012 at 2:53 PM


Alternatively you can treat a type as "any"... that has the unfortunetly side effect that we lose all intellisense and "type" checking.

 

class MyClass {
	constructor(public title: string){
	}
}

function Append(value: string){
	var div = document.createElement('div')
	div.innerHTML = value;
	document.body.appendChild(div)
}

//Option 1
var obj1: any = new MyClass("Dyns 2 Title");
Append(obj1.title);
obj1.name = "Dyns Name";
Append(obj1.name);

//Option 2
var obj2: MyClass = new MyClass("Dyns 2 Title");
Append(obj2.title);
(<any>obj2).name = "Dyns Name";
Append((<any>obj2).name);

//Option 3
var obj3: MyClass = new MyClass("Dyns 2 Title");
Append(obj3.title);
var dyn3: any = obj3;
dyn3.name = "Dyns Name";
Append(dyn3.name);

 

Ofc. it would be somewhat nice that we could do that explicit 'behave as any' in a more easy way... But I am not sure where what the right route would be either, there is certainly implications to it.

I think I would prefere to have option 2 made easier if anything, so that I in a single call on the type could say "now I wan't to use you as any"... (<any>"...") is allot of extra stuff to type. That also ensures that if I change the name of title on MyClass, the compiler would catch this... (oh and if the compiler in that special case would strip away the surrounding ()'s as well >.<)

Ofc. adding to the interfaces is possible as well, but sometimes thats also just allot of extra typing for extremely simple cases.

Last thing to consider though, is how it would align the ECMA 6... I am not sure how classes will be treated there at all, but as I understand it, classes will merely be syntactic sugar so we should still be able to add new properties etc. to an object on the fly.

Mar 6, 2013 at 12:21 PM
Just to add something to this old discussion, as I ran into a use case.
I have been working on some core parts for Angular, and specifying routes is done with "objects", say like:
  $routeProvider.when('/home', { /* some object. */ });

  interface IRouteProvider {
    when(path: string, route: any);
  }
We have to use any here as technically the object has no constraints as such, but if that object contains certain properties, those begin to have meaning. So to serve "discoverablity" it would be nice to define route as IRoute...
  interface IRouteProvider {
    when(path: string, route: IRoute);
  }

  interface IRoute {
    controller?: (...args: any[]) => any;
    templateUrl?: any;
    //...Ect
  }
But since the consumer of the API can actually add additional properties which would just be passed on. We have to stick to "any" and that means we can't give him some help in the definition files.

So the motivation in this case becomes an API developer that want's to provide an interface to the consumers of his API, and signal to them that "This parameter can contain these parameters, and if it does ill use them for something, but you can add your own on top of that and ill hand those back to you when I activate your route by handing you this entire object"...

Back to the fact that Interfaces are open ended, means that the consumer could ofc. just define those extra things on the interface, inherit from the original IRoute interface and be ok with it, or he can treat the parameters he pass into the "when" method with "any"... So the same workarounds are still there, but this adds another case where it could be useful.