40

In Angular it's technically possible to write class methods as ES2015 arrow functions, but I have never actually seen someone do it. Take this simple component for instance:

@Component({
  selector: 'sample'
})
export class SampleComponent {
  arrowFunction = param => {
    // Do something
  };
  normalFunction(param) {
    // Do something
  }
}

This works without any issues. Are there any differences? And why should or shouldn't I use this?

Gideon
  • 1,954
  • 3
  • 22
  • 29

3 Answers3

68

The points made in this React answer are still valid in Angular, any other framework or vanilla JavaScript/TypeScript.

Class prototype methods are ES6, class arrow methods aren't. Arrow methods belong to class fields proposal and not a part of existing specs. They are implemented in TypeScript and can be transpiled with Babel as well.

It's generally preferable to use prototype method() { ... } than arrow method = () => { ... } because it's more flexible.

Callbacks

The only real opportunity that arrow method provides is that it it can be seamlessly used as a callback:

class Class {
  method = () => { ... }
}

registerCallback(new Class().method);

If prototype method should be used as a callback it should be additionally bound, this should be preferably be done in constructor:

class Class {
  constructor() {
    this.method = this.method.bind(this);
  }

  method() { ... }
}

registerCallback(new Class().method);

A decorator like bind-decorator can be used in TypeScript and ES Next to provide more concise alternative to method binding in constructor:

import bind from 'bind-decorator';

class Class {
  @bind
  method() { ... }
}

Inheritance

Arrow method restricts child classes to use arrow methods too, otherwise they won't be overridden. This creates a problem if an arrow was overlooked:

class Parent {
  method = () => { ... }
}

class Child extends Parent {
  method() { ... } // won't override Parent method
}

It's not possible to use super.method() in child class because super.method refers to Parent.prototype.method, which doesn't exist:

class Parent {
  method = () => { ... }
}

class Child extends Parent {
  method = () => {
    super.method(); // won't work
    ...
  }
}

Mixins

Prototype methods can be efficiently used in mixins. Mixins are useful for multiple inheritance or to fix problems in TypeScript method visibility.

Since arrow method isn't available on class prototype, it can't be reached from outside the class:

class Parent {
  method = () => { ... }
}

class Child extends OtherParent { ... }
Object.assign(Child.prototype, Parent.prototype) // method won't be copied

Testing

A valuable feature that prototype methods provide is that they are accessible before class instantiation, thus they can be spied or mocked in tests, even if they are called right after construction:

class Class {
  constructor(arg) {
    this.init(arg);
  }

  init(arg) { ... }
}

spyOn(Class.prototype, 'init').and.callThrough();
const object = new Class(1);
expect(object.init).toHaveBeenCalledWith(1);

This is not possible when a method is an arrow.

TL;DR: the choice between prototype and arrow class methods seems like a matter of taste, but in reality the use of prototype methods is more far-sighted. You may usually want to avoid arrow class methods, unless you are sure that they will cause no inconvenience. Don't forget to use bind on prototype methods if you pass them as callbacks.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Really great answer, but in your TL;DR, you don't necessarily need to use .bind if you use a fat arrow to call the prototype based method when inside the class itself? – Richard Watts Jul 06 '18 at 12:07
  • @RichardWatts Prototype method `bind` and arrow are mutually exclusive. Do you mean something like `arrowMethod = () => this.anotherPrototypeMethod()`? No, `bind` is not needed here. – Estus Flask Jul 06 '18 at 12:10
  • Sorry no, I wasn't clear enough. I'm in a class and I have normal class based methods defined `public mySuccessMethod(success) {...} public myErrorMethod(error) {...}` An async call has happened and in my subscribe (rxjs) I have `asyncCall.subscribe(success => mySuccessMethod(success), error => myErrorMethod(error))` in this `.subscribe` I have absolutely no need to use `.bind` due to the fact I'm using fat arrow which gives the correct this context? – Richard Watts Jul 06 '18 at 12:16
  • 1
    @RichardWatts That's correct, you don't really need `bind` here. But it's beneficial to use bound methods like `.subscribe(this.myMethod)` because 1) you don't need to enumerate the args, especially if there's more than one arg (and `(...args) => myMethod(...args)` doesn't play well with TS types) 2) if this piece of code is called often, you don't need to create arrow functions every time it's called 3) this benefits unit testing a bit, you can assert `expect(obs.subscribe).toHaveBeenCalledWith(obj.myMethod)` - something you can't do with anonymous functions. Apart from that, arrows are ok. – Estus Flask Jul 06 '18 at 12:31
  • Thanks for the response, so in my example I showed, I have no need to enumerate args as the parameter passed into those methods is just an object from rx. I kind of get the argument for using bind if you're bothered about the use of arrow functions every time but I guess thats down to personal preference as I would prefer that over `.bind()`. RE testing - doesn't this depend? As in it shouldn't matter if you're doing the correct mocking and stubbing? Sorry not trying to sound smart! – Richard Watts Jul 06 '18 at 12:51
  • @RichardWatts This surely depends. In this particular case I would probably go with arrow, exactly because a function has 1 arg and isn't created too often. Although I usually use decorators like [this one](https://github.com/NoHomey/bind-decorator) so binding is as simple as `@bind`, takes less chars to type. – Estus Flask Jul 06 '18 at 13:18
  • @RichardWatts The problem with testing anonymous functions is that we can't be sure they do only things we expect them to do, and it would be harder to prevent something like `.subscribe(result => { this.methodA(); this.methodB(); })`. As for `.subscribe(this.methodA)`, it's very straightforward, you stub `methodA`, test an observable, and if a test fails, you can be sure that it's only an observable that can be blamed, not `methodA` or `methodB`. They are considered different units here, I believe this provides better isolation for them. – Estus Flask Jul 06 '18 at 13:25
  • @EstusFlask Can you elaborate the callback section or point to resource which explains it better. A detziled example on "use bind on prototype methods if you pass them as callbacks" would be helpful. thanks! – Saksham Jul 13 '19 at 12:46
  • @Saksham Sadly, I don't have any resource at hand. This is evident from how `this` works in JS. If a function is used as a callback, i.e. it loses original context when called, it needs to be bound to proper `this` context. This can be done either via an arrow or `bind`. Since class prototype methods aren't arrows, they need to be bound with `bind`. – Estus Flask Jul 13 '19 at 13:10
  • @EstusFlask can you look at https://www.typescriptlang.org/play/index.html?target=1#code/MYGwhgzhAEAKCmAnCB7AdtA3gWAFDWjTAFt4AuaCAF0QEs0BzaAXmgCIIwBrCACxLYBuPATANyhAK7EARkhbQArMNwjoAcXhUAguIBK8BgAoAlFjUEIWgCq1SKSVSOmWAPnP4CXq1Vv3HzmbM7jieXl7A6Kgg8AB0ICjGVLy0ELFEpAA00MmpsWLwJirhBAC+2QCMAAw1RRbQ5dDVtcUNeKV4eABuYIjQKDIAVhUKaPAA7nBIqGimKngDw7GaOvqGc3hAA and suggest how to implement this? – Saksham Jul 13 '19 at 13:42
  • @Saksham You don't need to implement this. GetAgeReg is not a callback, it's called with proper context. – Estus Flask Jul 14 '19 at 05:56
4

A good use case of class arrow functions are when you want to pass a function to another component and save the context of current component in the function.

@Component({

   template:`
        I'm the parent
       <child-component></child-component>

  `
})
export class PerentComponent{

   text= "default text"
   arrowFunction = param => {
    // Do something
    // let's update something in parent component ( this)

    this.text = "Updated by parent, but called by child"
  };
}

@Component({

   template:`
        I'm the child component

  `
})
export class ChildComponent{
   @Input() parentFunction;

   ngOnInit(){
      this.parentFunction.()
   }
}

 <parent-component></parent-component>

In above example, child is able to call parent component's function and text will correctly be updated, where as if I just change the parent a bit to be :

export class PerentComponent{

   text= "default text"
   arrowFunction (){
    this.text = "This text will never update the parent's text property, because `this` will be child component  "
  };
}
R. Richards
  • 24,603
  • 10
  • 64
  • 64
Milad
  • 27,506
  • 11
  • 76
  • 85
1

There's just one case where you have to refrain from using arrow functions if you need to do AOT compilation, as documented here

When configuring a module, you can't use arrow functions.

❌ DONT:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { Routes, RouterModule } from '@angular/router';

@NgModule({
  imports: [
    BrowserModule,
    RouterModule,
    HttpModule,
    RouterModule.forRoot([], { errorHandler: (err) => console.error(err) })
  ],
  bootstrap: [
    AppComponent
  ],
  declarations: [
    AppComponent
  ]
})
export class AppModule {}

✅ DO:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { Routes, RouterModule } from '@angular/router';

function errorHandler(err) {
  console.error(err);
}

@NgModule({
  imports: [
    BrowserModule,
    RouterModule,
    HttpModule,
    RouterModule.forRoot([], { errorHandler })
  ],
  bootstrap: [
    AppComponent
  ],
  declarations: [
    AppComponent
  ]
})
export class AppModule {}
Andrei Matracaru
  • 3,511
  • 1
  • 25
  • 29
  • As mentioned in the article, this only seems to apply during configuration of a module, suggesting that arrow functions as class methods would be OK for AOT. – jzig Nov 07 '18 at 15:56