The value of this
this is an implicit parameter, and the call decides what goes in it, not the function.
Ordinary functions and methods receive it. Calling obj.method() fills it with obj, the receiver, which is whatever sits to the left of the dot. Calling method() with nothing to the left fills it with undefined. Arrow functions do not have one at all: they treat this as an ordinary variable and read it from the enclosing scope, the way the closures page's a function carries its birth scope reads any other name, which is why theirs is called lexical and everyone else's is called dynamic.
Every surprise below is that one sentence playing out. If you take nothing else: this belongs to the call site, so moving a function changes it and copying a reference to it loses it.
Create programs/the-value-of-this.test.ts for this reference and keep it open. Start it with the assertion helpers used on this page:
import {
assert,
assertEquals,
assertStrictEquals,
assertThrows,
} from "@std/assert";
Follow the page as you add and revise the runnable examples below that import.
the receiver is whatever is left of the dot
Deno.test("the receiver is whatever is left of the dot", () => {
const jill = {
whoAmI(): unknown {
return this;
},
};
assertStrictEquals(jill.whoAmI(), jill);
const extracted = jill.whoAmI;
assertStrictEquals(extracted(), undefined);
});
Check programs/the-value-of-this.test.ts
running 1 test from ./programs/the-value-of-this.test.ts
the receiver is whatever is left of the dot ... ok (0ms)
ok | 1 passed | 0 failed (1ms)
Same function, called twice, two different values of this. Nothing about whoAmI changed between those lines; only the call did. The first call has jill to the left of the dot, so this is jill. The second line is the whole problem in miniature: extracted is the identical function object, and it has lost its receiver, because a plain variable read does not carry one.
a plain call supplies undefined
Deno.test("a plain call supplies undefined", () => {
function ordinary(this: unknown): unknown {
return this;
}
assertStrictEquals(ordinary(), undefined);
const borrowed = { name: "Jack", ordinary };
assertStrictEquals(borrowed.ordinary(), borrowed);
});
a plain call supplies undefined ... ok (0ms)
In a module, which is always strict, a function called with no receiver gets undefined rather than the global object. The second half makes the reverse point: drop any ordinary function into an object, and calling it through that object makes it a method, receiver and all. Functions are not owned by objects; they are values that objects can hold, which is the functions page's the three roles of an ordinary function seen from the this side. The this: unknown in the signature is a TypeScript annotation the step TypeScript will not let you touch this unexamined explains; for now, read it as "any receiver welcome".
an ordinary function inside a method loses this
Deno.test("an ordinary function inside a method loses this", () => {
const holder = {
probe(): [unknown, unknown] {
const inner = function (this: unknown): unknown {
return this;
};
const arrow = (): unknown => this;
return [inner(), arrow()];
},
};
const [fromInner, fromArrow] = holder.probe();
assertStrictEquals(fromInner, undefined);
assertStrictEquals(fromArrow, holder);
});
an ordinary function inside a method loses this ... ok (0ms)
Both functions are written inside the method, one line apart, and they disagree about this. The ordinary function has a this of its own, so calling it plainly fills that with undefined, shadowing the method's. The arrow has none, so this resolves outward like any other variable and finds the method's holder. This is the actual reason to prefer arrow functions inside methods, and it is a better reason than brevity: an arrow cannot make this mistake, because there is nothing to shadow.
the loss announces itself as a TypeError
In real code the shadowed this does not return undefined politely. It throws:
Deno.test("the loss announces itself as a TypeError", () => {
const reader = {
name: "log.txt",
read(): string {
const grab = function (this: { name: string }): string {
return this.name;
};
// @ts-expect-error: The 'this' context of type 'void' is not assignable to method's 'this' of type '{ name: string; }'.
return grab();
},
};
assertThrows(
() => reader.read(),
TypeError,
"Cannot read properties of undefined (reading 'name')",
);
});
the loss announces itself as a TypeError ... ok (0ms)
That pinned message is worth memorising, because it is what the mistake looks like from the outside. Cannot read properties of undefined where you expected an object usually means a this that was filled in by the wrong call: grab() has nothing left of the dot, its this arrives as undefined, and undefined.name explodes one property-read later. Note that the checker refused first, in the @ts-expect-error: because grab declares what its receiver must be, the bare call is TS2684 before it is a TypeError, a payoff the last step returns to.
extraction is the same bug wearing a callback
counter.add works when called through the dot, so predict the total after handing the method itself to forEach:
Deno.test("extraction is the same bug wearing a callback", () => {
const counter = {
total: 0,
add(n: number): void {
this.total += n;
},
};
[1, 2].forEach(counter.add);
assertStrictEquals(counter.total, 3);
});
Check programs/the-value-of-this.test.ts
running 5 tests from ./programs/the-value-of-this.test.ts
...
extraction is the same bug wearing a callback ... FAILED (1ms)
ERRORS
extraction is the same bug wearing a callback => ./programs/the-value-of-this.test.ts:68:11
error: TypeError: Cannot read properties of undefined (reading 'total')
this.total += n;
^
FAILURES
extraction is the same bug wearing a callback => ./programs/the-value-of-this.test.ts:68:11
FAILED | 4 passed | 1 failed (3ms)
error: Test failed
The memorised message, live, and the checker said nothing on the way to it, because add's receiver was never written down. forEach(counter.add) reads the method out of the object and hands the bare function to forEach, which calls it with no receiver: the dot in counter.add does not travel with the value, it only applies to the call it is part of. Pin the crash and fix it with an arrow, which keeps a dot in the actual call:
Deno.test("extraction is the same bug wearing a callback", () => {
const counter = {
total: 0,
add(n: number): void {
this.total += n;
},
};
assertThrows(
() => [1, 2].forEach(counter.add),
TypeError,
"Cannot read properties of undefined (reading 'total')",
);
[1, 2].forEach((n) => counter.add(n));
assertStrictEquals(counter.total, 3);
});
extraction is the same bug wearing a callback ... ok (0ms)
This is the everyday version of the problem: passing a method somewhere as a callback. xs.forEach((x) => obj.add(x)) or xs.forEach(obj.add.bind(obj)), never xs.forEach(obj.add), which is the shape that looks tidiest and is wrong.
call, apply, and bind name the receiver
Deno.test("call, apply, and bind name the receiver", () => {
function describe(this: unknown, ...args: string[]): unknown[] {
return [this, ...args];
}
assertEquals(describe.call("hello", "a", "b"), ["hello", "a", "b"]);
assertEquals(describe.apply("hello", ["a", "b"]), ["hello", "a", "b"]);
const bound = describe.bind("hello", "a");
assertEquals(bound("b"), ["hello", "a", "b"]);
assertEquals(describe("a", "b"), [undefined, "a", "b"]);
});
call, apply, and bind name the receiver ... ok (0ms)
Three methods that every function has, and one idea between them: they make the implicit parameter explicit. call takes the receiver followed by the arguments. apply takes the receiver and the arguments as an array. bind takes the receiver and returns a new function with it fixed in place. The last assertion is the useful reframing: an ordinary call is a call with undefined as the receiver, so there is no special case, only a default.
apply mattered more before spread syntax existed. describe(...args) now covers most of what it was for, with the mechanics on the parameters and arguments page, and apply survives mainly in older code and in places where the receiver has to be dynamic too.
an arrow function ignores all three
An arrow reads this from its surroundings. Predict what call can do about that:
Deno.test("an arrow function ignores all three", () => {
const lexical = (): unknown => this;
assertStrictEquals(lexical.call("hello"), "hello");
assertStrictEquals(lexical.apply("hello"), undefined);
assertStrictEquals(lexical.bind("hello")(), undefined);
});
Check programs/the-value-of-this.test.ts
running 7 tests from ./programs/the-value-of-this.test.ts
...
an arrow function ignores all three ... FAILED (8ms)
ERRORS
an arrow function ignores all three => ./programs/the-value-of-this.test.ts:100:11
error: AssertionError: Values are not strictly equal.
[Diff] Actual / Expected
- undefined
+ "hello"
FAILURES
an arrow function ignores all three => ./programs/the-value-of-this.test.ts:100:11
FAILED | 6 passed | 1 failed (11ms)
error: Test failed
Not "throws", not "warns": silently ignores. An arrow has no this parameter to fill, so the receiver call supplies has nowhere to go, and the arrow returns its lexical this instead, which at this position in the file is the module's undefined, for a reason the last step measures. Correct the prediction to undefined:
an arrow function ignores all three ... ok (0ms)
This is worth knowing because it is a real failure mode when arrows meet older APIs. Anything that takes a callback and a thisArg, or documents that it calls your function with a particular receiver, cannot do that to an arrow. If the API wants to give you a receiver, write an ordinary function.
bind is also partial application
Deno.test("bind is also partial application", () => {
function add(a: number, b: number): number {
return a + b;
}
const add8 = add.bind(undefined, 8);
assertStrictEquals(add8(1), 9);
const sameThing = (...args: [number]) => add(8, ...args);
assertStrictEquals(sameThing(1), 9);
});
bind is also partial application ... ok (0ms)
Arguments after the receiver are fixed too, which makes bind a way to pre-fill parameters. The undefined first argument is the giveaway that you are using it for that and not for this, and it reads badly enough that the arrow spelling below it is usually the better choice. Keep bind for what only it does: fixing a receiver.
TypeScript will not let you touch this unexamined
Write a bare this inside an ordinary function and the checker stops you before any of the bugs above can happen:
Deno.test("TypeScript will not let you touch this unexamined", () => {
const unchecked = function () {
return this;
};
assertStrictEquals(unchecked(), undefined);
});
Check programs/the-value-of-this.test.ts
TS2683 [ERROR]: 'this' implicitly has type 'any' because it does not have a type annotation.
return this;
~~~~
at file:///programs/the-value-of-this.test.ts:122:14
An outer value of 'this' is shadowed by this container.
const unchecked = function () {
~~~~~~~~
at file:///programs/the-value-of-this.test.ts:121:23
error: Type checking failed.
TS2683, from noImplicitThis, which Deno turns on with the rest of strict, and even the secondary note teaches: the function container shadows the outer this, which is the mechanism of an ordinary function inside a method loses this restated by the checker. Given that the answer depends entirely on how someone calls the function, refusing to guess is the checker being honest rather than difficult. The way to answer is a this parameter, a parameter in the signature that is not a parameter at the call site:
Deno.test("TypeScript will not let you touch this unexamined", () => {
const unchecked = function () {
// @ts-expect-error: 'this' implicitly has type 'any' because it does not have a type annotation.
return this;
};
assertStrictEquals(unchecked(), undefined);
type Named = { name: string };
function greet(this: Named, greeting: string): string {
return `${greeting}, ${this.name}`;
}
const jack: Named & { greet: typeof greet } = { name: "Jack", greet };
assertStrictEquals(jack.greet("Hi"), "Hi, Jack");
assertStrictEquals(greet.call({ name: "Jill" }, "Yo"), "Yo, Jill");
assertThrows(
() => {
// @ts-expect-error: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Named'.
greet("Hi");
},
TypeError,
"Cannot read properties of undefined (reading 'name')",
);
});
TypeScript will not let you touch this unexamined ... ok (1ms)
Look at what the last pin buys. greet("Hi") is the extraction bug from earlier in this entry, and it is now TS2684, "The 'this' context of type 'void' is not assignable to method's 'this' of type 'Named'": the most common this mistake in JavaScript becomes a compile-time error, purely because you wrote down what the receiver has to be. Compare the forEach(counter.add) crash, which the checker let through because add never declared its receiver. That is the payoff worth taking away from this entry: this is unusually error-prone in JavaScript and unusually well handled by TypeScript, and the handling is opt-in, one function at a time.
at the top of a module, this is undefined
Deno.test("at the top of a module, this is undefined", () => {
assertStrictEquals(this, undefined);
assert(typeof globalThis === "object");
});
at the top of a module, this is undefined ... ok (0ms)
The step's callback is an arrow inside an arrow, so its this resolves outward all the way to module scope, and module-scope this is undefined. Module scope is not global scope, the fact the scope and declarations page measured in there is no global scope to fall into, and globalThis is the thing to name when you genuinely mean the global object. Do not reach for this in a standalone function: it is undefined, the checker will stop you, and what you wanted was either a parameter or globalThis.
In practice
- Use arrow functions for callbacks written inside a method so they inherit the surrounding receiver.
- Wrap or bind a method passed as a callback:
xs.forEach((x) => obj.add(x))orxs.forEach(obj.add.bind(obj)), notxs.forEach(obj.add). - Declare a
thisparameter on any function meant to be called with a receiver so extraction mistakes become type errors. - Use
bindfor receivers and arrows for pre-filled arguments.