this in TypeScript
Declare what this must be with a this parameter, see what noImplicitThis catches, and keep methods from losing their object when you pass them around.
- 01Write a this parameterSay what
thismust be in a function, and read the error TypeScript gives a call without it. - 02Fix a this with no typeExplain what
noImplicitThisreports, and where TypeScript already knowsthisfor you. - 03Pass methods safelyKnow when TypeScript catches a method that loses its object, and keep
thiswith an arrow function orbind.
A method that loses its cart
A shop's cart has an add method. Inside it, this means the cart the method was called on: the object before the dot. Copy the method out of the cart and call it on its own, and there is no cart any more (the Losing this lesson shows why). Here is that bug in TypeScript:
class Cart { items: string[] = []; add(item: string) { this.items.push(item); }} const cart = new Cart();const add = cart.add;add("Tea");console.log(cart.items);Lines 1 to 6 are a Cart class (Class basics covers classes). Line 2 is a field: every cart starts with an empty list of item names, typed string[]. Line 3 is the add method, and line 4 pushes the item onto this.items.
Line 8 makes a cart. Line 9 copies the method into add, without the cart. Line 10 calls it with nothing before the dot. Code inside a class always runs in strict mode, so this is undefined, and line 4 throws TypeError: Cannot read properties of undefined (reading 'items'). Line 11 never runs. The list under the code is empty: TypeScript assumes a class's method is always called on an object of that class.
One small change makes TypeScript check it. Line 3 now starts with this: Cart:
class Cart { items: string[] = []; add(this: Cart, item: string) { this.items.push(item); }} const cart = new Cart();const add = cart.add;add("Tea");console.log(cart.items);this: Cart looks like a parameter, but it isn't one you pass. It tells TypeScript what this must be whenever add runs. Now line 10 gets an error, on purpose, because it's the bug:
The 'this' context of type 'void' is not assignable to method's 'this' of type 'Cart'means "this call has no object before the dot, andaddneeds aCartasthis". TypeScript calls the missing objectvoid, its word for "nothing here".- The code still throws when it runs. The error is a warning for you, before anyone clicks a button.
add, called two ways. With cart. in front, this is the cart. Called on its own, as on line 10, this is undefined, and reading this.items throws.A shop has a form that says "add this item to my order". When you hand it in, "my" means you. If someone finds a copy on the floor and drops it in the box, there is no one behind "my", and the clerk can't do anything with it. So the shop prints a line at the top, "for card holders only", and the clerk checks it before taking any form.
- In real life: A form that says "add this to my order"
- In TypeScript: A method that uses
this - In real life: "My" means whoever hands the form in
- In TypeScript:
thisis the object before the dot - In real life: A form found on the floor has no one behind "my"
- In TypeScript: A method called on its own has no
this - In real life: A line at the top: "for card holders only"
- In TypeScript:
this: Cart - In real life: The clerk reads that line before taking the form
- In TypeScript: TypeScript checks every call against
this: Cart
In JavaScript, this is decided by how a function is called. A this parameter, like this: Cart, tells TypeScript what this must be, so it can check every call. TypeScript can only check calls it sees, so when you hand a method to other code, keep the object with an arrow function or bind.
The this parameter
A this parameter always goes first in the brackets, and it works in any function, not only in classes. This function describes whichever product it's called on:
type Product = { name: string; price: number }; function describe(this: Product) { return `${this.name}: $${this.price.toFixed(2)}`;} const tea = { name: "Tea", price: 3, describe };console.log(tea.describe());Line 1 names the shape of a product: a name and a number price. Line 3 says describe's this must be a Product. Line 4 reads this.name and this.price, and TypeScript knows both, because it knows what this is. Line 7 puts the function into a tea object as a method; { describe } is short for { describe: describe }. Line 8 calls tea.describe() and prints Tea: $3.00.
A function's call method runs it with the this you choose (call, apply, and bind covers it). Try five ways to call describe on line 8:
type Product = { name: string; price: number }; function describe(this: Product) { return `${this.name}: $${this.price.toFixed(2)}`;} const tea = { name: "Tea", price: 3, describe };console.log(tea.describe());tea.describe()No errorsprints Tea: $3.00tea.describe() has tea before the dot, so this is the tea. It has a name and a price, which is what this: Product asks for. No errors, and it prints Tea: $3.00.
tea.describe() and describe.call(tea) both give this a product, so there are no errors. describe() gets the same error as the cart's line 10. describe.call({ name: "Cake" }) gets an ordinary argument error, because call's first argument is checked against Product, and this one has no price.
The table below lists the ways to call a function with a this parameter. Read each row as: this call, what this is, and what TypeScript checks.
| How you call it | What this is | What TypeScript checks |
|---|---|---|
tea.describe() | The object before the dot, tea. | That tea fits Product. |
describe() | Nothing. Strict code gets undefined. | Nothing can fit Product, so it reports an error. |
describe.call(tea) | The first argument, tea. | That the first argument fits Product. |
describe.bind(tea) | Always tea, in the new function bind makes. | That tea fits Product. The new function needs no this. |
Like every type, the this parameter is only there for TypeScript. It checks the calls, then removes it, so the browser gets a describe with no parameters at all:
this: Product in cart.ts. TypeScript checks every call against it, then removes it. The browser runs describe() with no parameters at all.function describe() {
return `${this.name}: $${this.price.toFixed(2)}`;
}
const tea = { name: "Tea", price: 3, describe };
console.log(tea.describe());Line 1 has empty brackets: this: Product is gone, and so is the type on line 1 of the TypeScript. The rest is the same code, and line 6 still prints Tea: $3.00. This is also why describe takes no argument for this: there was never a real parameter to fill.
noImplicitThis: a this with no type
What if a function uses this and you don't say what it is? Here is a click handler written as a plain function:
const button = document.createElement("button"); function markActive() { this.classList.add("active");//^?}button.addEventListener("click", markActive);button.click();console.log(button.className);Line 1 makes a <button>. Lines 3 to 6 are the handler, and line 4 adds the class active to this. Line 7 makes it the button's click handler (Introduction to events covers addEventListener), line 8 clicks the button, and line 9 prints active. The browser calls a click handler with the element as this, so the code works.
But markActive is a plain function, and code anywhere could call it in any way. TypeScript can't know what this will be, so it gives it the type any, which turns checking off. The query on line 5 shows any. The error on line 4 is on purpose: 'this' implicitly has type 'any' because it does not have a type annotation. In plain words: "you used this, and I would have to guess its type".
That error comes from a compiler option called noImplicitThis. The strict option, which every example here uses, turns it on. Try the handler with it on, with it off, and with a this parameter:
const button = document.createElement("button"); function markActive() { this.classList.add("active");//^?}button.addEventListener("click", markActive);button.click();console.log(button.className);anyon1 error: line 4prints activemarkActive is a plain function, so TypeScript can't know what this will be. With strict on, which turns on noImplicitThis, it marks line 4: 'this' implicitly has type 'any' because it does not have a type annotation. The query on line 5 shows any.
With noImplicitThis off, the error is gone, but this is still any. That's worse than an error, because nothing you do with this is checked. Here a typo gets through:
const button = document.createElement("button"); function markActive() { this.clasList.add("active");}button.addEventListener("click", markActive);button.click();Line 4 says clasList, with one s. TypeScript reports nothing, because this is any. When line 7 clicks the button, this.clasList is undefined, and the handler throws TypeError: Cannot read properties of undefined (reading 'add'). With this: HTMLButtonElement, TypeScript would have asked Did you mean 'classList'?
You don't need a this parameter everywhere. In an object's own methods, TypeScript works this out for you:
const settings = { theme: "dark", toggle() { this.theme = this.theme === "dark" ? "light" : "dark"; //^? },};settings.toggle();console.log(settings.theme);Lines 1 to 7 make a settings object with a theme and a toggle method. Line 4 flips the theme between dark and light. The query on line 5 shows that TypeScript knows this is the object itself: this: { theme: string; toggle(): void; }. Line 8 calls settings.toggle(), and line 9 prints light. Class methods work the same way: inside them, this is an object made by the class.
The table below shows where TypeScript knows this. Read each row as: where you write this, what TypeScript thinks it is, and whether you need a this parameter.
| Where you write this | What TypeScript thinks this is | Do you need a this parameter? |
|---|---|---|
A method in an object, like toggle() { … } | The object's own type. | No. |
A method in a class, like add(item: string) { … } | An object made by the class. | Only if the method might be called on its own. |
A plain function, like a click handler | It can't know. With noImplicitThis, that's an error. | Yes. |
| An arrow function | The this of the code around it. | You can't give it one. |
Methods passed as callbacks
Methods often go to other functions as callbacks: to forEach, to a click listener, to a timer (Callbacks covers the idea). The other function calls yours later, usually with nothing before the dot. Does a this parameter catch that?
class Cart { items: string[] = []; add(this: Cart, item: string) { this.items.push(item); }} const cart = new Cart();["Tea", "Cake"].forEach(cart.add);console.log(cart.items.length);Line 3 has this: Cart. Line 9 hands cart.add to forEach, which calls it once per item with nothing before the dot. It throws a TypeError, and TypeScript reports nothing.
TypeScript compares this types only when both sides have one. The callback type forEach asks for, (value, index, array) => void, says nothing about this, so there's nothing to compare Cart with. setTimeout's callback type says nothing about this either. So does the general type that addEventListener falls back to when a listener doesn't fit its precise one.
A function can say what this its callback gets. Below, line 1 describes a callback with (this: void, item: string) => void: a function that takes one item and is called with nothing before the dot. Line 4 calls it exactly that way. Now try four ways to hand over cart.add on line 15:
type ItemCallback = (this: void, item: string) => void; function forEachItem(items: string[], callback: ItemCallback) { for (const item of items) callback(item);} class Cart { items: string[] = []; add(this: Cart, item: string) { this.items.push(item); }} const cart = new Cart();forEachItem(["Tea", "Cake"], cart.add);console.log(cart.items.length);throws a TypeErrorforEachItem(["Tea", "Cake"], cart.add);1 error: line 15forEachItem says its callback has this: void: it calls the callback with nothing before the dot. add needs this: Cart, so TypeScript marks line 15: The 'this' types of each signature are incompatible. Run it anyway and add throws a TypeError.
Pass cart.add gets an error, because add needs a Cart and forEachItem promises nothing. The full message says Type 'void' is not assignable to type 'Cart'. Wrap it in an arrow and Bind it to the cart both keep the cart, and print 2. Use forEach is back to no errors and a crash. So write this: void in callback types you design, and wrap or bind a method whenever you hand it over.
bind makes a new function whose this is fixed. TypeScript works out the new function's type too:
class Cart { items: string[] = []; add(this: Cart, item: string) { this.items.push(item); }} const cart = new Cart();const addToCart = cart.add.bind(cart);// ^?addToCart("Tea");console.log(cart.items.length);Line 9 binds add to the cart. The query on line 10 shows const addToCart: (item: string) => void: it still takes an item, and the this part is gone, because it's always the cart. Line 11 calls it on its own, and line 12 prints 1.
Arrow functions and this
An arrow function has no this of its own. Inside it, this means whatever it meant in the code around the arrow (Arrow functions and this covers it). That makes an arrow function the simplest way to write a method that can't lose its object:
class Cart { items: string[] = []; add = (item: string) => { this.items.push(item); };} const cart = new Cart();["Tea", "Cake"].forEach(cart.add);console.log(cart.items.length);Line 3 makes add a field whose value is an arrow function. A field's starting value is worked out as each cart is made, where this is the new cart. So this add keeps that cart as its this for good. Line 9 hands cart.add to forEach with no wrapper, and line 10 prints 2.
this from the call, so forEach leaves it undefined. An arrow function has no this of its own: it uses the one where it was made, inside the cart.Because an arrow function has no this, it can't have a this parameter either:
type Product = { name: string; price: number }; const describe = (this: Product) => { return `${this.name}: $${this.price}`;};Line 3 gets An arrow function cannot have a 'this' parameter. Line 4 gets two more errors: TypeScript uses the this outside the arrow, not your Product. This code sits at the top of a module file, where this is undefined, so both reads get Object is possibly 'undefined'. Use function when you want a this parameter.
The same rule catches people out in objects:
const settings = { theme: "dark", label: () => `Theme: ${this.theme}`,};console.log(settings.label());Line 3 makes label an arrow function. You might expect its this to be settings, but an arrow uses the this around the object, not the object. TypeScript marks line 3 on purpose: Object is possibly 'undefined': it checks the code as a module file, where the this around the object is undefined, and reading from it would throw. This page runs the code as a plain script instead, where that this is the browser's window, which has no theme, so line 5 prints Theme: undefined. Either way, it isn't settings. For a method that uses this, write label() { … }, like toggle earlier.
So there are two good ways to write add. The table below compares them. Read each row as a question, then each way's answer.
| Question | A method with this: Cart | An arrow function field |
|---|---|---|
| What do you write? | add(this: Cart, item: string) { … } | add = (item: string) => { … }; |
Can you pass cart.add around? | Only where the other side allows it. TypeScript stops you where this: void is asked for. | Yes, anywhere. this is always the cart. |
| Does it help plain JavaScript callers? | No. The check is removed with the types. | Yes. The arrow works the same in plain JavaScript. |
How many copies of add are there? | One, shared by every cart. | One per cart, made when the cart is made. |
A search box that reacts to typing
A search box on a shop page updates the results as the shopper types. A class holds the box and what has been typed, and listens for the box's input event. Here is the version many people write first:
class SearchBox { query = ""; input = document.createElement("input"); constructor() { this.input.addEventListener("input", this.update); } update() { this.query = this.input.value.trim(); console.log(`Searching for ${this.query}`); }} const search = new SearchBox();search.input.value = " green tea ";search.input.dispatchEvent(new Event("input"));Line 2 starts query empty, and line 3 makes the <input> box. Line 6, in the constructor, says: when the box changes, call this.update. Lines 9 to 12 are update: line 10 reads the box's text, trims the spaces, and stores it, and line 11 prints it. Lines 15 to 17 make a search box, put " green tea " in it, and send an input event, as typing would.
TypeScript reports nothing, but it throws. The browser calls an event handler with the element as this, so inside update, this is the <input>, not the search box. The <input> has no input property, so line 10 throws TypeError: Cannot read properties of undefined (reading 'value'). Even this: SearchBox on line 9 wouldn't help here: addEventListener falls back to its general type, which accepts any listener and says nothing about this. The fix is an arrow function:
class SearchBox { query = ""; input = document.createElement("input"); constructor() { this.input.addEventListener("input", this.update); } update = () => { this.query = this.input.value.trim(); console.log(`Searching for ${this.query}`); };} const search = new SearchBox();search.input.value = " green tea ";search.input.dispatchEvent(new Event("input"));Line 9 makes update an arrow function field, made as each search box is made. Its this is always the search box, whoever calls it. Line 10 reads the box's text, and line 11 prints Searching for green tea. Handlers in classes, for clicks, typing, and timers, are where arrow function fields are used most.
function show(this: { label: string }) { return this.label; } show();function show(this: { label: string }) { return this.label; } show.call({ label: "Tea" });function show() { return this.label; }const tea = { label: "Tea", show() { return this.label; } };const show = (this: { label: string }) => "Tea";class Cart { items: string[] = []; add = (item: string) => { this.items.push(item); }; } ["Tea"].forEach(new Cart().add);class Cart { items: string[] = []; add(item: string) { this.items.push(item); } } ["Tea"].forEach(new Cart().add);function later(run: (this: void) => void) { run(); } const cart = { count: 0, add(this: { count: number }) { this.count += 1; } }; later(cart.add);
Sort each line by what TypeScript says about it. Each line is checked on its own, with the lesson's strict settings.
Common mix-ups
- "A this parameter is a real parameter I have to pass." No. It only tells TypeScript what
thismust be, and it's removed before the code runs. You still callcart.add("Tea")with one argument. - "With this: Cart, TypeScript catches every lost this." No. It checks calls it can see, and callback types that say what
thisis.forEach(cart.add)and event listeners get no error. - "noImplicitThis changes what this is when the code runs." No. Compiler options only change what TypeScript checks. The browser decides
thisthe same way either way. - "Every function that uses this needs a this parameter." No. In an object's or a class's own methods, TypeScript already knows
this. Plain functions, like handlers, are the ones that need it. - "An arrow function is always the safe choice for this." No. In a class field it keeps the object, but as a method in an object literal it gets the
thisaround the object, not the object. - "bind loses the types." No. TypeScript knows
cart.add.bind(cart)is a(item: string) => void, and it checks that the object you bind fits.
Practice exercises
Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
Three calls, one mistake. Which line will TypeScript mark?
type User = { firstName: string };
function greet(this: User) {
return `Hi, ${this.firstName}`;
}
const asha = { firstName: "Asha", greet };
console.log(asha.greet());
console.log(greet.call(asha));
console.log(greet());type User = { firstName: string }; function greet(this: User) { return `Hi, ${this.firstName}`;} const asha = { firstName: "Asha", greet };console.log(asha.greet());console.log(greet.call(asha));console.log(greet()); // line 10: no user before the dotLine 8 has asha before the dot, and line 9 passes asha to call, so both give this a user. Line 10 calls greet() with nothing, so TypeScript reports The 'this' context of type 'void' is not assignable to method's 'this' of type 'User'.
TypeScript marks line 4. Add a this parameter so it knows what this is. What does the fixed code print?
type User = { firstName: string }; function greet() { return `Hi, ${this.firstName}`;} const asha = { firstName: "Asha", greet };console.log(asha.greet());type User = { firstName: string }; function greet(this: User) { return `Hi, ${this.firstName}`;} const asha = { firstName: "Asha", greet };console.log(asha.greet());this: User tells TypeScript that this has a firstName, so line 4 is checked and the error is gone. Line 8 calls it on asha and prints Hi, Asha.
A button counts its clicks. Fix the error on line 6 with a this parameter. What does the fixed code print?
const button = document.createElement("button");let clicks = 0; function countClick() { clicks += 1; this.textContent = `Clicked ${clicks} times`;}button.addEventListener("click", countClick);button.click();button.click();console.log(button.textContent);const button = document.createElement("button");let clicks = 0; function countClick(this: HTMLButtonElement) { clicks += 1; this.textContent = `Clicked ${clicks} times`;}button.addEventListener("click", countClick);button.click();button.click();console.log(button.textContent);The handler is attached to a button, so this is an HTMLButtonElement. With that written on line 4, TypeScript knows this.textContent is a string. Two clicks print Clicked 2 times.
TypeScript marks line 15. Change only that line so each product is saved. What does the fixed code print?
type ProductCallback = (this: void, product: string) => void; function forEachProduct(products: string[], callback: ProductCallback) { for (const product of products) callback(product);} class Wishlist { saved: string[] = []; save(this: Wishlist, product: string) { this.saved.push(product); }} const wishlist = new Wishlist();forEachProduct(["Tea", "Cake", "Jam"], wishlist.save);console.log(wishlist.saved.length);type ProductCallback = (this: void, product: string) => void; function forEachProduct(products: string[], callback: ProductCallback) { for (const product of products) callback(product);} class Wishlist { saved: string[] = []; save(this: Wishlist, product: string) { this.saved.push(product); }} const wishlist = new Wishlist();forEachProduct(["Tea", "Cake", "Jam"], (product) => wishlist.save(product));console.log(wishlist.saved.length);The arrow function calls wishlist.save(product) with the wishlist before the dot, so this is the wishlist. It needs no this itself, so it fits this: void. Three products are saved, and it prints 3.
TypeScript is happy with this code. What does it throw when it runs? Then change like so line 9 works as it is.
class LikeCounter { likes = 0; like() { this.likes += 1; }} const counter = new LikeCounter();["post-1", "post-2", "post-3"].forEach(counter.like);console.log(counter.likes);class LikeCounter { likes = 0; like = () => { this.likes += 1; };} const counter = new LikeCounter();["post-1", "post-2", "post-3"].forEach(counter.like);console.log(counter.likes);The starter throws a TypeError: this is undefined inside like, so this.likes can't be read. As an arrow function field, like keeps the counter as its this wherever it goes, and the fixed code prints 3.
TypeScript marks line 3. Fix the profile so its greeting uses its own name. What does the fixed code print?
const profile = { firstName: "Asha", greeting: () => `Hi, ${this.firstName}`,};console.log(profile.greeting());const profile = { firstName: "Asha", greeting() { return `Hi, ${this.firstName}`; },};console.log(profile.greeting());As a method, greeting gets the object before the dot as this, and TypeScript knows it has a firstName. The error is gone, and it prints Hi, Asha.
Check your understanding
For each question, ask two things: what is before the dot when the function runs, and does TypeScript see that call? The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does a this parameter, like
this: Cart, do?Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript mark in this order label code?
Read the code, then predicttype Order = { id: number }; function orderLabel(this: Order) { return `Order #${this.id}`; } const order = { id: 7, orderLabel }; console.log(order.orderLabel()); console.log(orderLabel());Choose an answer to see the explanation.
Question 3 of 7What happens with this cart and forEach?
Read the code, then predictclass Cart { items: string[] = []; add(this: Cart, item: string) { this.items.push(item); } } const cart = new Cart(); ["Tea", "Cake"].forEach(cart.add); console.log(cart.items.length);Choose an answer to see the explanation.
Question 4 of 7Why does TypeScript mark line 2 of this banner code?
Read the code, then predictfunction hideBanner() { this.hidden = true; }Choose an answer to see the explanation.
Question 5 of 7What does this cart with an arrow function field print?
Read the code, then predictclass Cart { items: string[] = []; add = (item: string) => { this.items.push(item); }; } const cart = new Cart(); ["Tea", "Cake", "Jam"].forEach(cart.add); console.log(cart.items.length);Choose an answer to see the explanation.
Question 6 of 7What does TypeScript say about line 3 of this banner object?
Read the code, then predictconst banner = { text: "Sale today", show: () => this.text, }; console.log(banner.show());Choose an answer to see the explanation.
Question 7 of 7Which change lets forEachItem(items, cart.add) run without losing the cart?
Choose an answer to see the explanation.
Key takeaways
- In JavaScript,
thisis decided by the call. A method called on its own, or handed to other code, can lose its object. - A
thisparameter, likethis: Cart, goes first in afunction's brackets. TypeScript checks every call against it, then removes it. noImplicitThis, part ofstrict, reports athiswith no type in a plain function. Methods of objects and classes don't need one.- TypeScript catches a lost
thisonly where it can compare: a direct call, or a callback type that saysthis: void.forEachand event listeners don't. - To keep the object, wrap the call in an arrow function, use
bind, or make the method an arrow function field. - An arrow function has no
thisof its own, so it can't have athisparameter, and it's the wrong choice for a method in an object literal.
Remember the one-liner.
Tell TypeScript what this must be, and keep the object with you whenever you hand a method away.
Coming next: Narrowing with typeof & truthiness. It shows how TypeScript reads your if checks to work out a more precise type for a value.