Skip to content

Closures ​

Arrow functions and function expressions capture variables by reference, exactly as in JavaScript. A local that a closure captures and something reassigns lives in a heap cell shared by the frame and every closure; variables that are never reassigned are captured by value, which costs nothing extra. for (let ...) and for...of give each iteration its own binding.

ts
// Closures capture variables by reference: a counter factory, a callback that reassigns a
// captured local, and per-iteration `let` bindings exactly as in JavaScript.
function counter(start: number): () => number {
  let n = start;
  return (): number => {
    n += 1;
    return n;
  };
}

const next = counter(10);
next();
console.log(next(), next());

let total = 0;
[1, 2, 3].forEach((x: number): void => {
  total += x;
});
console.log("total", total);

const printers: (() => string)[] = [];
for (let i = 0; i < 3; i++) {
  printers.push((): string => `i=${i}`);
}
console.log(printers.map((p: () => string): string => p()).join(" "));

function compose(f: (x: number) => number, g: (x: number) => number): (x: number) => number {
  return (x: number): number => g(f(x));
}
const incThenDouble = compose(
  (x: number): number => x + 1,
  (x: number): number => x * 2,
);
console.log(incThenDouble(5));
text
12 13
total 6
i=0 i=1 i=2
12

Functions are values: they can be stored in arrays and fields, passed, returned and compared with ===.

Limits today ​

  • this inside a function expression is rejected; use an arrow function or a class method.
  • A function declaration nested inside another function or block is rejected (CS1000); bind an arrow function with const, or move it to the top level.
  • Built-ins such as String or Math.floor cannot be passed as values (CS1246); wrap them: xs.map((x) => String(x)).
  • Reading a narrowed variable that a closure reassigns, after a call that might run the closure, is rejected (CS1241), because tsc's narrowing can be stale there.

Next: Map and Set.