TypeScript This Typing — Complete Guide
In this tutorial, you will learn about TypeScript This Typing. We cover key concepts, practical examples, and best practices to help you master this topic.
TypeScript's this typing gives you compile-time control over the notoriously tricky this context in JavaScript — you can specify the expected this type for functions, use polymorphic this types in classes, and catch this-related bugs before they crash at runtime.
What You'll Learn
- The
thisparameter in functions - Polymorphic
thistypes for method chaining - Typing
thisin callbacks and event handlers - Common
thispitfalls and how to avoid them
Why It Matters
JavaScript's this is one of the most common sources of bugs — losing this context in callbacks, event handlers, and setTimeout calls. TypeScript's this parameter lets you document and enforce the expected this context, preventing entire categories of runtime errors.
Real-World Use
The Doda Browser extension API uses typed this parameters extensively — event handlers like browser.tabs.onCreated.addListener expect this to be the extension context. Durga Antivirus Pro's class methods use this as a return type for Builder pattern implementations.
Learning Path
flowchart LR A[Decorators] --> B[This Typing] B --> C[Index Signatures] B --> D[You Are Here] C --> E[Type Guards] E --> F[Narrowing]
The this Parameter
In TypeScript, you can declare the expected type of this as the first parameter of a function (compile-time only, erased at runtime):
function greet(this: { name: string }): string {
return `Hello, ${this.name}`;
}
const alice = { name: "Alice" };
const bob = { name: "Bob" };
// Bind the function to an object
const boundGreet = greet.bind(alice);
console.log(boundGreet()); // Hello, Alice
// Direct call — TypeScript checks `this`
// greet(); // Error: The 'this' context of type 'void' is not assignable to type '{ name: string }'
Think of this parameter as: "When you call this function, make sure this has at least these properties."
In Interfaces
interface ClickHandler {
(this: HTMLElement, event: MouseEvent): void;
}
function handleClick(this: HTMLElement, event: MouseEvent): void {
this.style.backgroundColor = "red"; // this is typed as HTMLElement
}
document.querySelector("button")?.addEventListener("click", handleClick);
The ThisType Utility
ThisType<T> tells TypeScript what this should be in an object literal's methods:
type ObjectDescriptor<D, M> = {
data?: D;
methods?: M & ThisType<D & M>;
};
function createApp<D, M>(desc: ObjectDescriptor<D, M>): D & M {
return { ...desc.data, ...desc.methods } as any;
}
const app = createApp({
data() {
return {
count: 0,
name: "App",
};
},
methods: {
increment() {
this.count++; // typed as number (from data)
},
greet() {
return `Hello from ${this.name}, count = ${this.count}`;
},
reset() {
this.count = 0;
},
},
});
app.increment();
app.increment();
console.log(app.greet()); // Hello from App, count = 2
Polymorphic this Type
When a class method returns this, TypeScript infers the most derived type — enabling fluent method chaining:
class Calculator {
constructor(protected value: number = 0) {}
add(n: number): this {
this.value += n;
return this;
}
subtract(n: number): this {
this.value -= n;
return this;
}
getResult(): number {
return this.value;
}
}
class ScientificCalculator extends Calculator {
multiply(n: number): this {
this.value *= n;
return this;
}
power(n: number): this {
this.value = Math.pow(this.value, n);
return this;
}
}
const calc = new ScientificCalculator(10);
const result = calc
.add(5) // Returns ScientificCalculator
.multiply(2) // Returns ScientificCalculator
.power(3) // Returns ScientificCalculator
.subtract(100) // Returns ScientificCalculator
.getResult(); // number
console.log(result); // (10 + 5) * 2 = 30, 30^3 = 27000, 27000 - 100 = 26900
this in Callbacks
Arrow functions capture this from the surrounding scope. Regular functions do not:
class Timer {
private seconds = 0;
startWithArrow(): void {
setInterval(() => {
this.seconds++; // `this` captured from class instance
}, 1000);
}
startWithRegular(): void {
setInterval(function() {
// this.seconds++; // Error: 'this' implicitly has type 'any'
// At runtime, `this` would be the global object or undefined
}, 1000);
}
}
Typing Callback this
interface Counter {
count: number;
increment(this: Counter): void;
}
const counter: Counter = {
count: 0,
increment(this: Counter) {
this.count++;
},
};
function repeat(fn: (this: Counter) => void, times: number, context: Counter): void {
for (let i = 0; i < times; i++) {
fn.call(context);
}
}
repeat(counter.increment, 5, counter);
console.log(counter.count); // 5
this in Event Handlers
class UIComponent {
private clicks = 0;
constructor(private element: HTMLElement) {
// Arrow function — `this` is the class instance
this.element.addEventListener("click", (event) => {
this.handleClick(event);
});
}
private handleClick(this: HTMLElement, event: MouseEvent): void {
// `this` should be the element that was clicked
console.log(`Clicked on ${this.tagName}`);
}
// Alternative: bind in constructor
private boundHandler = (event: MouseEvent): void => {
this.clicks++;
};
}
Class Method Binding
When passing class methods as callbacks, this is lost:
class Logger {
private prefix = "[LOG]";
log(message: string): void {
console.log(`${this.prefix} ${message}`);
}
// Arrow function property — always bound to instance
boundLog = (message: string): void => {
console.log(`${this.prefix} ${message}`);
};
}
const logger = new Logger();
const fn = logger.log;
// fn("test"); // Runtime error: Cannot read properties of undefined (reading 'prefix')
const boundFn = logger.boundLog;
boundFn("test"); // [LOG] test — works because arrow function captures this
noImplicitThis
Enable noImplicitThis (included in strict) to catch untyped this usage:
// With noImplicitThis: true
function badFunction() {
// console.log(this.name); // Error: 'this' implicitly has type 'any'
}
function goodFunction(this: { name: string }) {
console.log(this.name); // OK
}
Common Mistakes
1. Losing this in Callbacks
class Button {
label: string;
constructor(label: string) {
this.label = label;
}
handleClick(this: Button): void {
console.log(`Clicked: ${this.label}`);
}
}
const btn = new Button("Submit");
// This loses `this`:
setTimeout(btn.handleClick, 1000); // this is undefined
// Fix: bind or arrow function
setTimeout(btn.handleClick.bind(btn), 1000);
setTimeout(() => btn.handleClick(), 1000);
2. Using Arrow Function Methods When You Don't Need To
Arrow function properties are instance-specific (not on Prototype). Use them only when you need automatic this binding for callbacks.
3. Not Typing this in DOM Event Handlers
// Bad: this is implicitly any
element.addEventListener("click", function() {
this.style.color = "red";
});
// Good: type this as HTMLElement
element.addEventListener("click", function(this: HTMLElement) {
this.style.color = "red";
});
4. Confusing this Type with Function Return Type
this as a return type means "returns the current instance." It does not affect the function's return value type.
5. Forgetting That this Parameter Is Erased
The this parameter is compile-time only. In compiled JavaScript, it's gone. Only use it for Type Checking, not runtime logic.
Practice Questions
How do you specify the
thistype for a function? Addthis: Typeas the first parameter. It's compile-time only and erased in output.What is the polymorphic
thistype? When a method returnsthis, TypeScript infers the most derived class type, enabling fluent chaining with correct types.What does
noImplicitThisdo? It errors whenthisis used without a type annotation, preventing accidentalanytyping.How do arrow functions handle
thisdifferently from regular functions? Arrow functions capturethisfrom the surrounding lexical scope. Regular functions receivethisfrom the caller.
Challenge: Write a QueryBuilder class that uses polymorphic this type for method chaining. Include methods like select(...), from(...), where(...), and orderBy(...). Each method returns this for fluent usage.
FAQ
Mini Project: Fluent Query Builder
// src/query-builder.ts
class QueryBuilder<T extends Record<string, unknown>> {
private selectFields: (keyof T)[] = [];
private tableName: string = "";
private conditions: string[] = [];
private orderField: keyof T | "" = "";
private orderDirection: "ASC" | "DESC" = "ASC";
private limitCount: number = 0;
select(...fields: (keyof T)[]): this {
this.selectFields = fields;
return this;
}
from(table: string): this {
this.tableName = table;
return this;
}
where(condition: string): this {
this.conditions.push(condition);
return this;
}
orderBy(field: keyof T, direction: "ASC" | "DESC" = "ASC"): this {
this.orderField = field;
this.orderDirection = direction;
return this;
}
limit(count: number): this {
this.limitCount = count;
return this;
}
build(): string {
const fields = this.selectFields.length > 0
? this.selectFields.join(", ")
: "*";
let query = `SELECT ${fields} FROM ${this.tableName}`;
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(" AND ")}`;
}
if (this.orderField) {
query += ` ORDER BY ${String(this.orderField)} ${this.orderDirection}`;
}
if (this.limitCount > 0) {
query += ` LIMIT ${this.limitCount}`;
}
return query;
}
}
interface User {
id: number;
name: string;
email: string;
age: number;
role: string;
}
const query = new QueryBuilder<User>()
.select("id", "name", "email")
.from("users")
.where("age > 18")
.where("role = 'admin'")
.orderBy("name", "ASC")
.limit(10)
.build();
console.log(query);
// SELECT id, name, email FROM users WHERE age > 18 AND role = 'admin' ORDER BY name ASC LIMIT 10
What's Next
Now explore index signatures for dynamic property access:
| Lesson | Description |
|---|---|
| {{< ref "/programming-languages/typescript/22-decorators" >}} | Review decorators |
| {{< ref "/programming-languages/typescript/24-index-signatures" >}} | Index signatures, Record, dynamic properties |
| {{< ref "/programming-languages/typescript/25-type-guards" >}} | Type guards and discriminated unions |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro