12

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

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
  • 2
    *"note that with ES5, this was easy to do"* You can do exactly the same thing: [ES6 - declare a prototype method on a class with an import statement](https://stackoverflow.com/q/32699889/218196) – Felix Kling Oct 09 '17 at 16:12
  • yes, but this is a perfect example of ES6 being less dynamic and more rigid than ES5..the technique with the import statement is ES5 syntax not ES6 syntax. – Alexander Mills Oct 09 '17 at 17:54
  • 2
    Uh? Everything that is valid in ES5 is also valid in ES6. If you mean syntax that was introduced in ES6, sure. But there is no way to write a JavaScript program without using syntax that was introduced before ES6... so that argument is kind of silly. – Felix Kling Oct 09 '17 at 17:56
  • dude I don't know why we are arguing about this....it is a limitation of ES6 classes, plain and simple. If you are on some JS committee, please fix this. – Alexander Mills Oct 09 '17 at 17:56
  • *"the problem with using TestBlock.prototype.startSuite = ..., is that in that case, I cannot simply return the class on line"* But you are not required to put the return statement there. You can do `return TestBlock;` after the declaration. – Felix Kling Oct 18 '17 at 20:07
  • yep but that code is less clean less beautiful and keeps me up at night – Alexander Mills Oct 18 '17 at 21:12

5 Answers5

10

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() {...}
}
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • should be `=> Function` not `=> void` ? – Alexander Mills Oct 13 '17 at 19:23
  • Using `Car.prototype.printName` is ES5 syntax - I am looking for ES6/TS version of this, to be specific. – Alexander Mills Oct 13 '17 at 20:08
  • 1
    The available options are all there. I've added the notion of mixins that are more complicated and require to change how makePrintName works, yet they are natural to TS. It isn't just deprecated 'ES5' syntax. It's JS, a single language. It is low-level but perfectly valid in ES6 and TS development if it fits the situation (and it fits this one). A class is a function and thus you can extend its prototype through `prototype`, simple as that. I guess Felix Kling already explained this in the comments above. – Estus Flask Oct 13 '17 at 22:57
  • My code would be a lot "cleaner" if I could return the class declaration from the enclosing function, instead of having to add the prototype functions and then return the class after adding the prototype methods using ES5 syntax. It's a clear limitation of ES6 classes. – Alexander Mills Oct 14 '17 at 01:12
  • @AlexanderMills I don't see what limitation you're talking about because ES6 classes are limited by design, they are syntactic sugar for *most frequent* conventional uses of `function` as a class. And what you're trying to do is edge case. I had to do this only a couple of times in TS and I used `prototype` assignment because that's what it is for. Any way, if you're uncomfortable with prototype, you can use mixin class instead, I guess that's what you asked about. – Estus Flask Oct 16 '17 at 11:25
  • Well, there is a clear limitation, that we all see. I am not sure how much of an edge case this is. Seems like a common type of problem, where you'd want to assign the method instead of declare it. – Alexander Mills Oct 16 '17 at 16:56
  • 1
    @AlexanderMills why would you use `Function` instead of `() => void`. Using `Function` is imprecise... – Aluan Haddad Oct 17 '17 at 02:56
  • @AlexanderMills Missed `() => void` question, indeed, it's like Aluan Haddad explained. I don't see this as a limitation. As I stated, it's edge case, so nobody cared about sugar syntax because `prototype` is totally valid in ES6. I personally had to deal with it in ES6 class directly only a couple of times (besides decorators), and its necessity was caused by badly designed third-party code. – Estus Flask Oct 18 '17 at 00:09
  • @AlexanderMills There's a chance that you have XY problem and don't need `makePrintName` at all. And it isn't supposed to look sweet - because adding a method to class prototype dynamically *is mix-in technique*. If `makePrintName` is first-party, this could be possibly managed with class inheritance instead - either mixin like `makePrintNameMixin` or not. – Estus Flask Oct 18 '17 at 00:12
  • F*ck that...with ES5 it's easy and advisable to use this pattern...ES6 syntax does not give you the ability to do what I want. It's not an X/Y problem. It's a syntax deficiency with ES6. – Alexander Mills Oct 18 '17 at 00:51
  • @AlexanderMills ES5 and ES6 and not two different languages. They are same language. It's JS. `prototype` is crucial part of it. I'm not sure what answer you expected, but I don't think that any better solution can be suggested besides the ones that were listed in this question. It may be possible to do the thing you're trying to do by using the full potential of ES6+ but you've intentionally narrowed down your choices to assigning `makePrintName` result to class prototype. That's why it may be XY problem. – Estus Flask Oct 18 '17 at 01:50
  • It seems like we are just arguing over semantics..ES6 classes are a wrapper, but the wrapper is apparently a leaky wrapper or leaky abstraction. – Alexander Mills Oct 18 '17 at 01:52
  • @AlexanderMills Possibly yes. Any way, if you will be interested in how the thing you're trying to do can be done in more idiomatic to ES6 (and more importantly, TS, because it has its limitations) fashion, feel free to ask a new question that will reflect what you're really trying to achieve with makePrintName(foo, bar, baz). I'm not always happy with some `class` syntax limitations, but once you know the options well, you pick the best one. – Estus Flask Oct 18 '17 at 02:01
  • is this answer better than @jcalz? this one seems more complicated – Alexander Mills Oct 20 '17 at 19:36
  • 1
    @AlexanderMills I've listed all the options that were practical in my opinion. If you don't like messing with `prototype`, pick the last one. As for jcalz's answer, it results in creating dummy function that will be discarded but still requires to maintain proper signature for it. Having it like `@setMethod(makePrintName(foo, bar, baz)) printName() {}` and calling it with `this.printName(1)` will result in type error in TS because it wasn't supposed to be called with an arg. Didn't mention it because it looks more like decorator misuse. – Estus Flask Oct 20 '17 at 20:55
4

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

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
Aleksey L.
  • 35,047
  • 10
  • 74
  • 84
2

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?

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • I think @estus may have suggested something similar? check out their answer thx – Alexander Mills Oct 16 '17 at 02:35
  • 1
    That was a class decorator that modifies the class; this is a method decorator that only touches the method on which it is declared. They are similar but not the same, so I figured I'd mention it. – jcalz Oct 16 '17 at 02:54
1

Why don't you try something like this

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

    }
}
Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
Gautam
  • 815
  • 6
  • 15
0

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'));
douxsey
  • 2,999
  • 1
  • 12
  • 11
  • Right, and where is the syntactic sugar for assigning to prototype using a non-inline function!? – Alexander Mills Oct 15 '17 at 07:24
  • If you are going to answer in TypeScript, do so with reasonable types. – Aluan Haddad Oct 17 '17 at 02:53
  • Yup we can do this in a better for the type but here the concern was how to add prototype function to a class so we abstracted the type definition for more focus on the problem – douxsey Oct 17 '17 at 09:04