Skip to content

TypeScript This Typing — Complete Guide

DodaTech Updated 2026-06-28 8 min read

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 this parameter in functions
  • Polymorphic this types for method chaining
  • Typing this in callbacks and event handlers
  • Common this pitfalls 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

  1. How do you specify the this type for a function? Add this: Type as the first parameter. It's compile-time only and erased in output.

  2. What is the polymorphic this type? When a method returns this, TypeScript infers the most derived class type, enabling fluent chaining with correct types.

  3. What does noImplicitThis do? It errors when this is used without a type annotation, preventing accidental any typing.

  4. How do arrow functions handle this differently from regular functions? Arrow functions capture this from the surrounding lexical scope. Regular functions receive this from 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

Does the `this` parameter exist at runtime?

No. It is a TypeScript-only feature. The first this parameter is removed during compilation.

Can I use `this` as a return type in interfaces?

Yes: interface Builder { setValue(v: string): this; }

What is the difference between `this` and `self` in TypeScript?

this is the JavaScript keyword for the current context. self is not a TypeScript concept (though some codebases use it as a captured alias).

How do I type the `this` context of a callback?

Use the this parameter in the callback's type: type Handler = (this: HTMLElement, event: Event) => void;

Can I use `this` typing in interfaces describing object methods?

Yes. Interface methods can declare this parameters: { onClick(this: Button): void }

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