TypeScript allows passing class methods to functions that expect a lambda, but it won’t work at runtime. Consider this code:
function applyTwice(f: (x: number) => number, x: number): number { return f(f(x)); }
Suppose we have class Calculator like this:
class Calculator { private factor: number; constructor(factor: number) { this.factor = factor; } multiply(x: number): number { return x * this.factor; } }
We can then do:
const calculator = new Calculator(3); const result = applyTwice(calculator.multiply, 2); console.log(result);
It will compile, but it will blow up at runtime with Cannot read properties of undefined (reading 'factor')
.
When calculator.multiply
is passed in this manner, the magic this
parameter becomes undefined
this to calculator
, as follows:
const calculator = new Calculator(3); const result = applyTwice(calculator.multiply.bind(calculator), 2); // returns 2*3*3 = 18 console.log(result);
This works as expected, but looks kind of redundant.
TypeScript behaves a little bit like C++ in this situation, where class method also needs to be explicitly bound to a variable:
#include <iostream> #include <functional> class Calculator { public: explicit Calculator(int factor) : factor_(factor) {} int multiply(int x) const { return x * factor_; } private: int factor_; }; // applyTwice applies a function f to x twice: f(f(x)) int applyTwice(const std::function<int(int)>& f, int x) { return f(f(x)); } int main() { Calculator calculator(3); auto boundMultiply = [&calculator](int x) { return calculator.multiply(x); }; int result = applyTwice(boundMultiply, 2); // 3 * (3 * 2) = 18 std::cout << result << std::endl; return 0; }
Unlike TypeScript, if we try to simply pass calculator.multiply
to applyTwice
, it will generate an error. Exact error message depends on the compiler, but the main point is it won’t compile.
// GCC: invalid use of non-static member function 'int Calculator::multiply(int) const' int result = applyTwice(calculator.multiply, 2);
Python, Java, and C# automatically bind class methods to the variable, so the code works as expected out of the box.
Python:
class Calculator: def __init__(self, factor): self.factor = factor def multiply(self, x): return x * self.factor def apply_twice(f, x): return f(f(x)) calculator = Calculator(3) result = apply_twice(calculator.multiply, 2) # 3 * (3 * 2) = 18 print(result)
Java:
import java.util.function.IntUnaryOperator; public class Main { // Method that applies a function twice public static int applyTwice(IntUnaryOperator f, int x) { return f.applyAsInt(f.applyAsInt(x)); } static class Calculator { private final int factor; public Calculator(int factor) { this.factor = factor; } public int multiply(int x) { return x * factor; } } public static void main(String[] args) { Calculator calculator = new Calculator(3); int result = applyTwice(calculator::multiply, 2); // 3 * (3 * 2) = 18 System.out.println(result); } }
C#:
using System; class Program { static int ApplyTwice(Func<int, int> f, int x) { return f(f(x)); } class Calculator { private int factor; public Calculator(int factor) { this.factor = factor; } public int Multiply(int x) { return x * factor; } } static void Main() { var calculator = new Calculator(3); int result = ApplyTwice(calculator.Multiply, 2); // 3 * (3 * 2) = 18 Console.WriteLine(result); } }
TypeScript behaves worse than most popular languages, because it neither binds the method automatically (Java, C#, Python), nor enforces the binding via the compiler (C++).
Thanks to ChatGPT for creating this example and quickly translating it into 5 different languages.