I'm trying to cleanly write an Angular custom $resource
extension as a factory as a TypeScript class using DefinatelyTyped (IResource
, IResourceClass
and friends).
According to Misko Hevery resources are just constructor
functions so I was expecting to be able to define my $resource
as a regular class with some typesafe interfaces (INamedEntityResource
or INamedEntity
) and mixin the service definition but I can't seem to get the standard class methods on my NamedEntityResource prototype to end up on factory instances.
Is there a way of doing this with the constructor()
function or should I give up and just define the service in plain JavaScript?
declare module EntityTypes {
interface INamedEntity { }
}
module Services {
export interface INamedEntitySvc {
Name(params: {}, successCallback: (data: any, headers: any) => void, errorCallback: (data: any, headers: any) => void): EntityTypes.INamedEntity;
Clear(params: {}, value: EntityTypes.INamedEntity, successCallback: (data: any, headers: any) => void, errorCallback: (data: any, headers: any) => void): EntityTypes.INamedEntity;
}
// WILL have correct interface definition for the resource
export interface INamedEntityResource extends NamedEntityResource, INamedEntitySvc { }
export class NamedEntityResource {
// #1 DOESN'T WORK - These are on NamedEntityResource.prototype but don't end up on svc
public someMethod() { }
public someOtherMethod() { }
constructor($resource) {
var paramDefaults = {
};
var svc: INamedEntitySvc = $resource(getUrl(), paramDefaults, {
Name: <any>{ method: "GET", params: { action: "Name" } },
Clear: <any>{ method: "PATCH", params: { action: "Clear" }, headers: { 'Content-Type': 'application/json' } },
});
// THIS WORKS - but it's not a NamedEntityResource
svc["prototype"].someMethod = function () { }
svc["prototype"].someOtherMethod = function () { }
return <any>svc;
// #1 DOESN'T WORK THOUGH
return; // doesn't pick up methods on prototype
// #2 THIS DOESN'T WORK EITHER
NamedEntityResource["prototype"] = angular.extend(this["prototype"] || {}, svc["prototype"]);
return this;
}
}
// Registration
var servicesModule: ng.IModule = angular.module('npApp.services');
servicesModule.factory('NamedEntityResource', NamedEntityResource);
}
Further
So The purpose of this is to allow me to write a resource class{} with methods that will be annotated on every resource I load over HTTP. In this case, my INamedEntity
s.
This is the closest solution I've been able to get so far which does appear to work, but it feels really nasty.
module Services {
export interface INamedEntitySvc {
Name(params: {}, successCallback: (data: any, headers: any) => void, errorCallback: (data: any, headers: any) => void): EntityTypes.INamedEntity;
Clear(params: {}, value: EntityTypes.INamedEntity, successCallback: (data: any, headers: any) => void, errorCallback: (data: any, headers: any) => void): EntityTypes.INamedEntity;
}
// WILL have correct interface definition for the resource
export interface INamedEntityResource extends NamedEntityResource, INamedEntitySvc { }
export class NamedEntityResourceBase {
public someMethod() { }
public someOtherMethod() { }
}
// extend our resource implementation so that INamedEntityResource will have all the relevant intelisense
export class NamedEntityResource extends NamedEntityResourceBase {
constructor($resource) {
super(); // kind of superfluous since we're not actually using this instance but the compiler requires it
var svc: INamedEntitySvc = $resource(getUrl(), { }, {
Name: <any>{ method: "GET", params: { action: "Name" } },
Clear: <any>{ method: "PATCH", params: { action: "Clear" }, headers: { 'Content-Type': 'application/json' } },
});
// Mixin svc definition to ourself - we have to use a hoisted base class because this.prototype isn't setup yet
angular.extend(svc["prototype"], NamedEntityResourceBase["prototype"]);
// Return Angular's service (NOT this instance) mixed in with the methods we want from the base class
return <any>svc;
}
thisWontWork() {
// since we never actually get a NamedEntityResource instance, this method cannot be applied to anything.
// any methods you want have to go in the base prototype
}
}
// Registration
var servicesModule: ng.IModule = angular.module('npApp.services');
servicesModule.factory('NamedEntityResource', NamedEntityResource);
}
The trick was to;
- Hoist the methods I want on the service up into a base class because this.prototype isn't initialised by the time my constructor() function is called.
- Return
svc
which is the angular$resource
service from the constructor, which you can do in JavaScript of course, but it feels like really dirty duck-typing in TypeScript. - In order to get the methods on svc.prototype I extend that directly from my base class. This is particularly nasty as it means setting up the prototype every time an instance is created.
- The final pungent aroma to this sh** sandwich is I have to call super() on the constructor for the instance I'm throwing away just to get it to compile.
However, at the end of all that, I can add methods to NamedEntityResourceBase
and they'll appear in the prototype of all entities loaded from my HTTP resource.