Thursday, October 19, 2017

Create ES6/ESNext prototype function with different scope (not an inline function)

Leave a Comment

Ok say we have this:

class Car {     constructor(name) {         this.kind = 'Car';         this.name = name;     }      printName() {         console.log('this.name');     } } 

what I want to do is define printName, something like this:

class Car {     constructor(name) {         this.kind = 'Car';         this.name = name;     }      // we want to define printName using a different scope     // this syntax is close, but is *not* quite correct     printName: makePrintName(foo, bar, baz)  } 

where makePrintName is a functor, something like this:

exports.makePrintName = function(foo, bar, baz){    return function(){ ... } }; 

is this possible with ES6? My editor and TypeScript is not liking this

NOTE: using ES5, this was easy to do, and looks like this:

var Car = function(){...};  Car.prototype.printName = makePrintName(foo, bar, baz); 

Using class syntax, currently the best thing that is working for me, is this:

const printName = makePrintName(foo,bar,baz);  class Car {   constructor(){...}   printName(){     return printName.apply(this,arguments);   } } 

but that is not ideal. You will see the problem if you try to use class syntax to do what ES5 syntax can do. The ES6 class wrapper, is therefore a leaky abstraction.

To see the real-life use case, see:

https://github.com/sumanjs/suman/blob/master/lib/test-suite-helpers/make-test-suite.ts#L171

the problem with using TestBlock.prototype.startSuite = ..., is that in that case, I cannot simply return the class on line:

https://github.com/sumanjs/suman/blob/master/lib/test-suite-helpers/make-test-suite.ts#L67

5 Answers

Answers 1

The preferable ways to do this in JavaScript and TypeScript may differ due to limitations in typing system, but if printName is supposed to be a prototype method and not instance method (the former is beneficial for several reasons), there are not so many options.

Prototype method can be retrieved via accessor. In this case it should be preferably memoized or cached to a variable.

const cachedPrintName = makePrintName(foo, bar, baz);  class Car {     ...     get printName(): () => void {         return cachedPrintName;     } } 

And it can be lazily evaluated:

let cachedPrintName;  class Car {     ...     get printName(): () => void {         return cachedPrintName || cachedPrintName = makePrintName(foo, bar, baz);     } } 

Or it can be assigned to class prototype directly. In this case it should be additionally typed as class property because TypeScript ignores prototype assignments:

class Car {     ...     printName(): () => void; }  Car.prototype.printName = makePrintName(foo, bar, baz); 

Where () => void is the type of a function that makePrintName returns.

A way that is natural to TypeScript is to not modify class prototypes but extend prototype chain and introduce new or modified methods via mixin classes. This introduces unnecessary complexity in JavaScript yet keeps TypeScript happy about types:

function makePrintNameMixin(foo, bar, baz){   return function (Class) {     return class extends Class {       printName() {...}     }   } }  const Car = makePrintNameMixin(foo, bar, baz)(   class Car {     constructor() {...}   } ); 

TypeScript decorator cannot be seamlessly used at this point because class mutation is not supported at this moment. The class should be additionally supplemented with interface to suppress type errors:

interface Car {   printName: () => void; }  @makePrintNameMixin(foo, bar, baz) class Car {     constructor() {...} } 

Answers 2

Just replace : with =

printName = makePrintName() 

: goes for type notation.

Edit
As noted in comments the above will not change the prototype. Instead, you can do this outside the class definition, using ES5 syntax:

// workaround: extracting function return type const dummyPrintName = !true && makePrintName(); type PrintNameType = typeof dummyPrintName;  class Car {     // ...     printName: PrintNameType; } Car.prototype.printName = makePrintName(); 

Playground

Answers 3

Another idea I haven't seen mentioned yet is to use a method decorator. The following decorator takes a method implementation and puts it on the prototype, as desired:

function setMethod<T extends Function>(value: T) {     return function (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>): void {       descriptor.value = value;       delete descriptor.get;       delete descriptor.set;     }; } 

Put it in a library somewhere. Here's how you'd use it:

class Car {     constructor(name) {         this.kind = 'Car';         this.name = name;     }      @setMethod(makePrintName(foo, bar, baz))     printName() {} // dummy implementation } 

The only downside is that you have to put a dummy implementation of the method with the right signature in the class, since the decorator needs something to decorate. But it behaves exactly as you want at runtime (it isn't an instance method which costs a new function definition for each instance, or an accessor which costs an extra function call at each use).

Does that help?

Answers 4

Why don't you try something like this

class Car {     constructor(name) {         this.kind = 'Car';         this.name = name;         this.printName = makePrintName(foo, bar, baz);      } } 

Answers 5

am I dont getting it or ..??

class keyword normally is just a syntax sugar of the delegate prototype in ES5. I just tried it in typescript

class Car { private kind: string; private name : string; printName :Function;     constructor(name) {                 this.kind = 'Car';                 this.name = name;             }     } var makePrintName = function (foo, bar, baz) {     return function () { console.log(foo, bar, baz); }; }; Car.prototype.printName = makePrintName('hello', 'world', 'did you get me'); var bmw = new Car('bmw'); bmw.printName(); console.log(bmw.hasOwnProperty('printName')); 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment