Sunday, 21 October 2018

Class Properties and Arrows

The other day I came across a piece of TypeScript code that confused me a bit. In a class definition, among normal method definitions, there was a line like this (the "oddThing" declaration):

class A{    
    
    public normalMethod(msg:string):string{
 //whatever
    }

    public oddThing = (msg:string):string => {
        //whatever
    }
}

So, what is that arrow function doing there? Well, it's called class properties and at some future version should make it into standard javascript. So we are declaring a property and initialising it inline, rather than in the constructor (something very common in C# or java). So that declaration is equivalent to doing this in the constructor:

constructor(){
    this.oddThing = (msg:string):string => {
        //whatever
    };
}

In the end, that declaration and inline initialization is pretty normal for data, but what seems odd is why to do that for a "code block" rather than just using a normal method definition?
Well, the idea is pretty clear if we think about the big difference between arrow functions and normal functions (that is what is used for normal methods), the value of "this". While in "normal functions" "this" is dynamic and depends on the object used to get the function and invoke it, in arrow functions "this" is static, it's bound to the function at the moment of the declaration (similar to doing a Function.prototype.bind). So if we intend to use "oddThing" mainly by passing it around (think of callbacks, event listeners...), and not through an instance of the class where it's defined, using this property declaration instead of a method is a good option (or we could just create a closure trapping the intended this or use "bind").

I've put some code together:

class Formatter{

    public format3: (string) => string;

    constructor(private txt1:string){
        //equivalent to what we do with format2
        this.format3 = (msg:string):string => {
            return this.txt1 + msg + this.txt1;
        };
    }

    //normal method
    public format1(msg:string):string{
        return this.txt1 + msg + this.txt1;
    }

    //class property assigned to an arrow function rather than using a method 
    public format2 = (msg:string):string => {
        return this.txt1 + msg + this.txt1;
    }
}

let formatter1 = new Formatter(" || ");

let formatFn = formatter1.format1;

console.log(formatFn("hi"));
//so PROBLEM, "this.txt1" is undefined

console.log("----------");

//we could use the typical closure trick to trap the intended "this" (formatter1)
formatFn = function(txt:string):string {
    return formatter1.format1(txt);
};
console.log(formatFn("hi"));

console.log("----------");

formatFn = formatter1.format1.bind(formatter1);
console.log(formatFn("hi"));

console.log("----------");

formatFn = formatter1.format2;
console.log(formatFn("hi"));

console.log("----------");

formatFn = formatter1.format3;
console.log(formatFn("hi"));

//output:
// undefinedhiundefined
// ----------
//  || hi || 
// ----------
//  || hi || 
// ----------
//  || hi || 
// ----------
//  || hi || 

Update 2019/05/10By the way, this technique is perfectly valid in C# assigning lambdas to a field/property. In C# you can assign lambdas to a field/property, but that lambda can not use "this", the compiler will spit a "this" is not available in the current context error.

class Person
{
 public string Name = "aa".ToUpper();
 Func NameToUpper;
 
 public Person(string name)
 {
  this.Name = name;
  this.NameToUpper = () => this.Name.ToUpper(); 
 }
 
 //does not compile:
 //"this" is not available in the current context
 //public Func NameToUpper2 = () => this.Name.ToUpper(); 
}


1 comment: