Working with this in TypeScript
11 min read
The this keyword, a concept that I still see cause confusion amongst developers, probably due to classes and OOP mostly being disregarded these days. However, it pays to have an understanding as it still lurks in codebases.
We can apply a general rule to help get us started:
Most of the time, the
thiskeyword points to the object that a function belongs to
Functions belonging to objects
We'll come back to some of the exceptions, but let's start with the most common case, which is when a function belongs to an object or a class instance:
const job = {
title: 'Frontend Developer',
display() {
console.log(this.title)
},
}
job.display() // Frontend Developer
This is one of the simplest examples, and we can see here that this refers to
the job object. In fact, it's possible to just simply replace the keyword with
job and it would behave the same:
const job = {
title: 'Frontend Developer',
display() {
console.log(job.title)
// --------- ^
},
}
Whilst its possible to reference the property directly with job, this is not a common approach. You are more likely to see the use of the this keyword.
A reason to prefer this can be demonstrated a bit more clearly when using a class and creating an object with the new keyword:
class Job {
title: string
constructor(title: string) {
this.title = title
}
display() {
console.log(this.title)
}
}
const admin = new Job('admin')
const receptionist = new Job('receptionist')
Now, because we're creating any number of Job objects we can't hard code a
reference to it in the display method, simply because we don't know about it
yet. Instead we use this as a way of saying "the object I belong to at
creation". In fact, we can see two references of this here:
- In the
constructorwe are saying "when creating the object withnew, add thetitleargument to that particular instance" - When
displayis called, log the title assigned to that same instance
This becomes even clearer when we take a look at the resulting objects in the console:



Here you can see that the display method is 'attached' to both instances, and
the this keyword allows it to work this way. In fact, methods created on the
prototype are shared across instances which we can prove with a simple
comparison:
// logs: true
console.log(receptionist.display === admin.display)
The first guideline
At this point we can create our first guideline:
The object before the
.is whatthiswill refer to when the function is called
Following on from the above if I were to call
receptionist.display() then I can assume that the this value in display
will indeed be the receptionist.
What is interesting though, is that this is decided when the function is
called, not when it is defined.
One way to think about it is that this behaves similar to a parameter
in a function, but it is added behind the scenes along with any other explicit
parameters that we define. For example:
receptionist.display(receptionist)
// not valid code
display(this) {
console.log(this.title)
}
We will see that TypeScript thinks this way as well.
So, can you lose the this reference? Consider this example:
const adminDisplay = admin.display
adminDisplay()
// Error: can't access property "title", this is undefined
By assigning the admin.display function to a separate variable we've now
bypassed the first guideline and there is no object proceeding the . anymore,
which means at runtime this does not point to anything. It's as if we've
called our function without a this parameter.
When this happens it will default to undefined and we see the error in the
console.
Luckily we have ways to assign whatever value we want to this to work round
such issues, but first let's take a step back and look at how TypeScript works
with this
Introducing this to TypeScript
Remember when I said earlier that you can think of this like a hidden function
parameter? With TypeScript it's as if we reveal that parameter and make it very
clear what to expect.
We can revisit our Job example to demonstrate:
class Job {
title: string
constructor(title: string) {
this.title = title
}
display() {
console.log(this.title)
}
}
In this example, we don't need to tell TypeScript anything, as it can infer what
the value of this is correctly because it's clear that display belongs to
instances of Job. Nice to see it following our guideline!
Next let's intentionally create a function that has no connection to Job:
function jobDisplay(this: Job, message: string) {
console.log(`${this.title} ${message}`)
}
The jobDisplay function is standalone, but it also expects to be able to
reference this.title which is found on instances of Job.
By using a special TS specific annotation we can tell it what to expect this
to be with the first parameter, then any further parameters are the actual
arguments to the function.
We can try to use it:
function jobDisplayWithMessage(this: Job, message: string) {
console.log(`${this.title} ${message}`)
}
jobDisplayWithMessage('is a good job')

TypeScript will now correctly warn us that this function was expecting this to
reference an instance of Job, but it's undefined here. This is much better
than the runtime error we had earlier.
Can this be fixed?
JS has a way to tell a function what this should be, using call, apply or
bind. We can fix this particular example with call:
const admin = new Job('admin')
function jobDisplayWithMessage(this: Job, message: string) {
console.log(`${this.title} ${message}`)
}
// logs: admin is a good job
jobDisplayWithMessage.call(admin, 'is a good job')
Overriding this with call or apply
Now seems a good time to take a closer look at two functions to allow changing
the value of this when a function is called.
call
As we saw above, call behaves just like calling a function but the first
argument is used to set the this context. All additional arguments are passed
one at a time:
someFunction.call(object, 'argument1', 'argument2')
If you need to pass arguments to it dynamically then the spread operator can be used:
someFunction.call(object, ...someValues)
The same technique can be used to pass the arguments from a function directly:
// using the `arguments` variable
// in a function declaration
function example() {
return someFunction.call(object, ...arguments)
}
// if it's an arrow function then we don't
// have `arguments` available
const example = (...args: unknown[]) => {
return someFunction.call(object, ...args)
}
Not being able to use arguments inside arrow functions is one reason you don't
often see it being used. Another limitation is that it is 'array-like', meaning
it lacks things like map and forEach.
These days there is no reason not to favour rest parameters instead.
apply
This behaves exactly the same as call, but accepts an array as the second
argument instead. Consider the two examples above, but using apply:
function example() {
return someFunction.apply(object, arguments)
}
// if it's an arrow function then we don't
// have `arguments` available
const example = (...args: unknown[]) => {
return someFunction.apply(object, args)
}
There isn't much difference between the two since the spread operator became
more widely supported, but I still favour using apply with an array to make
things more obvious at a glance.
Creating bound functions
The final part of this journey is to explore the creation of bound functions,
which will reveal the use case for the bind function.
We can start with a fresh example, a function that will display a greeting to a User:
type User = {
firstName: string
lastName: string
}
function greetUser(this: User, greeting: string, message: string) {
return `${greeting}, ${this.firstName} ${this.lastName}. ${message}`
}
And it can be used like this:
const john = {
firstName: 'John',
lastName: 'Smith',
}
// log: Hey, John Smith. Good to see you
greetUser.call(john, 'Hey', 'Good to see you')
Now imagine that we always wanted to greet John, instead of using call each
time and passing a reference to the john object we could create a new function
that is bound to it for every invocation:
function greetJohn(greeting: string, message: string) {
return greetUser.call(john, greeting, message)
}
// log: Hey, John Smith. Good to see you
greetJohn('Hey', 'Good to see you')
We can remove the boilerplate from greetJohn by using bind instead:
const greetJohn = greetUser.bind(john)
This behaves exactly like our version that uses call. This is a good way to
think of bind whenever you see it. A new function is returned that calls the
original one with a fixed value for the this keyword.
And this cannot be changed, just like you can't override the use of call
inside the first greetJohn:
const greetJohn = greetUser.bind(john)
// has no effect, and will log:
// "Hey, John Smith. Good to see you"
greetJohn.call(someOtherUser, 'Hey', 'Good to see you')
A more concrete example of bind
Now that we have some understanding of what bind can do, what would be a
realistic application of it? One good example relates to event handlers.
When attaching a handler function to an element using
addEventListener(), the value ofthisinside the handler will be a reference to the element. It will be the same as the value of thecurrentTargetproperty of the event argument that is passed to the handler.-- MDN
Consider this example, where we are creating a class that attaches a method as a click handler:
class ElementClick {
element: HTMLElement | null
message: string
constructor(selector: string) {
this.message = 'hello'
this.element = document.querySelector(selector)
// here we pass a reference to `handleElementClick`
this.element?.addEventListener('click', this.handleElementClick)
}
handleElementClick(event: MouseEvent) {
// this will log `undefined`
console.log(this.message)
}
}
new ElementClick('#button')
What we would actually like to happen here is that handleElementClick is able
to reference our ElementClick instance and log 'hello' to the console.
By default we can imagine that somewhere deep down in native JavaScript the
callback we pass to addEventListener is called with the this value as the
target element.
Instead, if we bind that handler it will work as expected:
constructor(selector: string) {
this.message = 'hello'
this.element = document.querySelector(selector)
this.element?.addEventListener('click', this.handleElementClick.bind(this))
// -------------------------------------------------------------- ^
}
handleElementClick(event: MouseEvent) {
// this will log 'hello'
console.log(this.message)
}
A common way to handle this problem is to use an arrow function for event
handlers (and other similar callbacks). They do not have a this binding and instead take the value from their parent scope.
By changing handleElementClick to an arrow function can achieve the same result as bind:
constructor(selector: string) {
this.message = 'hello'
this.element = document.querySelector(selector)
this.element?.addEventListener('click', this.handleElementClick)
}
handleElementClick = (event: MouseEvent) => {
console.log(this.message)
}
Partial application
Now that we have solidified an understanding of bind and how it can be used,
there is one more useful feature to look at, known as partial application. This
is when a new function is created that already has some of its arguments
pre-filled.
Let's take a step back to John and his greetings. If you can recall, we had
created a greetJohn function that was bound to the john object:
const john = {
firstName: 'John',
lastName: 'Smith',
}
function greetJohn(greeting: string, message: string) {
return greetUser.call(john, greeting, message)
}
// usage
greetJohn('Hey', 'Good to see you')
What if we were to wrap greetJohn with another function, and this time bind
one if its arguments instead of just the this value?
function welcomeJohn(message: string) {
return greetJohn('Welcome', message)
}
// log: 'Welcome, John Smith. How was your weekend?'
welcomeJohn('How was your weekend?')
Now whenever we call welcomeJohn it will always force greetJohn to accept
'Welcome' as its first argument. We can take this a step further still and
create another function that binds the message argument as well.
Let's insult poor John:
function welcomeJohn(message: string) {
return greetJohn('Welcome', message)
}
function greetJohnAndInsult() {
return welcomeJohn('You stink!')
}
// log: Welcome, John Smith. You stink!
greetJohnAndInsult()
Using bind for easy partial application
We saw earlier that bind removes the boilerplate of binding the this value.
Another thing it enables are additional arguments to be passed and this will
also be bound to your function for all future invocations. We can rewrite our
examples above:
const john = {
firstName: 'John',
lastName: 'Smith',
}
const greetJohn = greetUser.bind(john)
const welcomeJohn = greetJohn.bind(null, 'Welcome')
const greetJohnAndInsult = welcomeJohn.bind(null, 'You stink!')
Notice the use of null there when calling bind. This is a common pattern
when you want to ignore the this value and just set the bound arguments.
Recall earlier how we saw that you cannot override the value of this once it
is applied by bind.
In the above example that value has already been bound with greetJohn, so to
avoid confusion when creating welcomeJohn and greetJohnAndInsult we can just
use null.
A more realistic example might be binding values to an event handler in a component library like React:
const handleClick = (action: string, event: MouseEvent) => {
event.preventDefault()
sendToAnalyticsPlatform(action)
}
function Component() {
return <button onClick={handleClick.bind(null, 'add_user')}>Add user</button>
}
Here we're binding the event action 'add_user' into the click handler and you
will notice that it is placed before the event argument, which would usually be
the first and only argument.
However, it's far more common to just pass an anonymous function to onClick that will then
call sendToAnalyticsPlatform when invoked. I definitely prefer to keep my
event handling separate from the actual business logic that is triggered:
<button
onClick={(event) => {
event.preventDefault()
sendToAnalyticsPlaform('add_click')
}}
>
Add user
</button>
But this was a useful exercise to get a good understanding of bind and its
uses.
Resources
- Examples on StackBlitz to try out