Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~25 min· topic 6 of 7

Topic 2.6

this in Functions

In one line

In JavaScript, this depends on how a function is called, not where it is written. TypeScript lets you declare what this must be with a this parameter, and warns when it would be lost.

Think of it like this

The word "I". If you say "I am hungry", it means you. If your friend says the same sentence, "I" means your friend. The sentence did not change; the speaker did. **this is a function's "I":** it means whoever called the function, and that can change from call to call.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

this
A special value inside a function that depends on how the function was called.
this parameter
A fake first parameter, this: Type, that exists only for the type checker and is erased at runtime.
Method
A function stored in an object's property and called like object.method().
Detached method
A method copied out of its object into a variable, so there is nothing before the dot when you call it.
noImplicitThis
A strict option that reports an error when this would silently be any.
bind
A function method that returns a copy of the function with a fixed this.
Arrow function
A function written () => {} that has no this of its own and uses the one around it.
Strict mode
A JavaScript mode (always on in modules) where an unbound this is undefined instead of the global object.

Step by step

01this depends on the call

person.describe() has this set to person. const f = person.describe; f() has no object, so this is undefined. The function is the same; the call is different.

02Declare what this must be

Add this: Person as the first parameter. It doesn't take an argument: callers still pass only greeting.

Method calls on an object with the right shape are accepted. A bare call is rejected.

this.tswhole filets
type Person = { name: string };

function describe(this: Person, greeting: string) {
  return greeting + ", " + this.name;
}

03Let TypeScript tell you about a missing this

In a function with no declared this, writing this.name is an error under noImplicitThis. Declare it, or turn the function into a method.

04The detached method problem, and the fix

An object method knows its this only at a method call. Copy it into a variable and the link is gone, but TypeScript is silent unless the method declares a this type.

With inc(this: Counter) the copy is caught at compile time. bind is the runtime fix.

The detached method problem, and the fixdiagram
Rendering diagram…

05Arrow functions keep the surrounding this

Inside a method, use an arrow function for callbacks. It borrows the method's this. A regular function callback has its own this and loses the object.

Try it yourself

  1. 1

    Pass the wrong this

    In the first example change describe.call({ name: "Ravi" }, "Hello") to describe.call({ nick: "Ravi" }, "Hello"). Predict whether tsc accepts it. The object has no name, so call rejects it with TS2353 (an unknown property in an object literal); strictBindCallApply makes call check its first argument against the this type.

  2. 2

    Turn the arrow into a function

    In the last example change startArrow to use function (n) { return this.seconds + n; }. Predict the error, then change it back. Why does the arrow version have none?

  3. 3

    Remove the this parameter

    In the third example remove this: Counter from the type. Predict whether the detached call is still caught. It isn't: you are back to the silent crash from the second example.

Code & diagrams

A this parameter on a standalone function

person.describe works because the thing before the dot has a name. .call lets you choose this, and it is type-checked. The bare calls have no object, so tsc rejects them, and at runtime the first one crashes (the second never runs).

Console

(nothing was printed)

Expected console output

Hi, Asha
Hello, Ravi
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
The detached method hole: TypeScript says nothing

No type errors at all, and the program crashes. TypeScript knows this inside inc is the object, but it does not notice that you called it without the object.

Console

(nothing was printed)

Expected console output

Uncaught TypeError: Cannot read properties of undefined (reading 'count')
Close the hole with a this parameter, and bind

Now the detached call is a compile-time error. bound works: bind fixes this, and strictBindCallApply gives bound the right type. The last line still crashes at runtime, which is exactly what the error predicted.

Console

(nothing was printed)

Expected console output

1
2
Uncaught TypeError: Cannot read properties of undefined (reading 'count')
Arrow functions use the surrounding this

The arrow in startArrow borrows the method's this. The regular function in startOld gets its own this, and noImplicitThis reports it. (startOld is never called, because it would crash.)

Console

(nothing was printed)

Expected console output

[ 1, 2 ]

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use this in a plain function

At the bottom of the first example add three lines: function whoAmI() {, return this.name;, }.

terminal
$ Types panel
── what you'll see ──
index.ts(12,10): error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.

Break #2

Call a this-typed function without an object

Call describe("Hey") for function describe(this: Person, greeting: string).

terminal
$ Types panel
── what you'll see ──
index.ts(8,1): error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'.

Myth vs fact

Myth

this always means the object where the function is written.

Fact

For regular functions it means whatever is before the dot at the call. For arrow functions it means the this of the code around where they were created.

Myth

A this parameter is a real parameter I must pass.

Fact

It exists only for the type checker and is erased. Callers never pass it.

Myth

TypeScript will always catch a lost this.

Fact

Only when the function declares its this type. A method in an object literal or class that you detach is silent, as the second example showed.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    strictBindCallApply (part of strict) gives bind, call and apply precise types using CallableFunction in lib.es5.d.ts, so describe.call({ nick: "x" }, "Hi") is checked against the this parameter. Without it they are loosely typed and accept anything.

  • ▸

    The ThisParameterType<T> and OmitThisParameter<T> utility types (in lib.es5.d.ts) extract and remove the this parameter of a function type. ThisType<T> is a marker used inside object literals to set this for methods. Typings for options-object APIs (the Vue options API is the well-known one) use it so that methods inside the object see the right this.

  • ▸

    DOM types declare this explicitly. In lib.dom.d.ts, addEventListener's listener is typed (this: HTMLElement, ev: ...) => any and Window.ondevicemotion is (this: Window, ev: ...) => any, so a regular-function handler gets the right this without annotations (arrow functions still ignore it).

  • ▸

    Class methods used as callbacks lose this just like object methods. Arrow-function class fields (handle = () => {...}) capture this per instance at the cost of one function object per instance and of not being on the prototype. Pick per case.

Remember this

  1. 1

    The problem this solves: this changes depending on how a function is called, so a method taken out of its object can crash with no warning in plain JavaScript. Typing this turns that into an error you see before running. In JavaScript, a regular function's this is set at the moment it is called. In person.describe(), this is person (the thing before the dot). If you take the function out and call it alone (const f = person.describe; f()), there is nothing before the dot, and this is undefined (in modules and strict mode). The JavaScript course explains the full rules. This topic covers how TypeScript helps.

  2. 2

    A **this parameter** is a fake first parameter that only exists for the type checker: function describe(this: Person, greeting: string). It is erased when the code is compiled, so callers never pass it. It states "this function must be called as a method of something that looks like Person". Calls like person.describe("Hi") are checked against it, and a bare describe("Hey") is TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'. void there means "no object before the dot".

  3. 3

    With noImplicitThis (part of strict), using this in a function that has no declared this type is an error: TS2683: 'this' implicitly has type 'any' because it does not have a type annotation. Without it, this would silently be any and nothing would be checked. Either give the function a this parameter, or move the code into a method of an object or class where TypeScript can tell what this is.

  4. 4

    Methods in object literals and classes get their this type automatically: in const counter = { count: 0, inc() { this.count++ } }, TypeScript knows this is the object, so this.count is a number. But the checker only knows this when the method is called as a method; it does not follow the method when you copy it to another variable. const detached = counter.inc; detached() type-checks fine and then crashes at runtime with Cannot read properties of undefined (reading 'count'). That is a real hole in TypeScript.

  5. 5

    You can close the hole by giving the method a this parameter: inc(this: Counter). Then counter.inc() is accepted, and const detached = counter.inc; detached() is rejected with TS2684. .bind(counter) returns a new function with this fixed, and TypeScript understands it (strictBindCallApply, part of strict, types bind, call and apply).

  6. 6

    **Arrow functions have no this of their own.** They use the this of the code around them, fixed at the moment they are created. That makes them the right choice for callbacks inside methods: [1, 2].map((n) => this.seconds + n) works because the arrow reuses the method's this. A regular function (n) { ... } callback gets its own this (not the object), which TypeScript reports under noImplicitThis. This is why arrows are used for event handlers and callbacks in classes (and why class methods passed as handlers lose their this, a problem React's class components made famous).

Explain it without notes

01

How is this decided for a regular function versus an arrow function?

02

What is a this parameter, and what happens to it when the code is compiled?

03

Why does const f = obj.method; f() often crash, and how can TypeScript help?

04

What does noImplicitThis do?

Practice

01

Write function area(this: { width: number; height: number }): number that returns this.width * this.height. Call it through an object that has those fields and then with .call. What error does a plain area() call give?

02

Fix the crash: const logger = { prefix: "[app]", log(msg: string) { console.log(this.prefix + " " + msg); } }; const f = logger.log; f("hi");. Give two different fixes.

03

Inside an object method, use forEach to log each item of this.items with a prefix from this.prefix. Which kind of function must the callback be?

Trade-offs

  • ↔

    Arrow-function class fields fix lost-this bugs by binding per instance, but cost an extra function object per instance, and you can't override them with normal inheritance rules. Prototype methods with explicit bind or wrappers are cheaper but easier to get wrong.

  • ↔

    A this parameter makes misuse a compile-time error, but it ties the function to one shape of object. If the function doesn't really need an object, take the data as a normal parameter instead.

Done when you can

  • I can explain how this is decided for regular and arrow functions.

  • I can write a this parameter and explain that it disappears when compiled.

  • I can explain and fix TS2683 and TS2684.

  • I can explain how a method loses its this and name three fixes.

  • I can use an arrow function in a method callback to keep this.