open atlas
↑ Back to track
TypeScript type system, deep TS · 06 · 02

Typing this

How TypeScript types the hidden first parameter with `this: T` (erased at emit), why a method loses its `this` when passed as a callback, and how ThisType, polymorphic `this`, and this-based guards model fluent and stateful APIs.

TS Middle ◷ 14 min
Level
FoundationsJuniorMiddleSenior

A class component works perfectly until someone writes button.addEventListener("click", this.handleClick). At runtime, this inside handleClick is now the <button>, not the component, and reads of this.state are undefined. The bug is invisible in plain JavaScript until it crashes. TypeScript can catch it at compile time — but only if you understand that every method has a hidden first parameter named this, and that detaching a method strips it.

The fake first parameter

Why does TypeScript need a special mechanism for this at all? Because this is the one value in JavaScript that is not determined by the function’s definition — it is set anew at every call site, which means the compiler cannot infer it from the source location alone.

JavaScript functions receive an implicit receiver: the this value, determined by how the function is called, not where it is defined. TypeScript lets you type that receiver with a special first parameter named this. It looks like a parameter but it is not one — it is erased at emit and never appears in the call’s argument list.

interface Card { suit: string; value: number }

function describe(this: Card): string {
  return `${this.value} of ${this.suit}`;
}

const card: Card = { suit: "hearts", value: 10 };
describe.call(card); // ok — `this` is supplied via .call
describe();
// Error: The 'this' context of type 'void' is not assignable
//        to method's 'this' of type 'Card'.

The emitted JavaScript is just function describe() { return this.value + ...; } — the this: Card annotation has vanished. Its only job was to type-check the receiver at every call site. Under --noImplicitThis (included in strict), a this used inside a function with no annotation and no inferable container is flagged as an implicit any, forcing you to declare it.

Methods carry an inferred this — and lose it when detached

Inside an object method or a class method, TypeScript infers this to be the containing object/instance. The problem appears when you detach the method — assign it to a variable or pass it as a callback — because the binding is set by the caller, not the definition.

class Counter {
  count = 0;
  inc(this: Counter) {     // explicit this for safety
    this.count++;
  }
}

const c = new Counter();
c.inc();          // ok — receiver is `c`
const f = c.inc;  // detached
f();
// Error: The 'this' context of type 'void' is not assignable
//        to method's 'this' of type 'Counter'.

This is the lost-this problem. Under the strictBindCallApply and method-this checks, TypeScript flags the unbound call. The runtime fixes are the usual ones — c.inc.bind(c), or an arrow-function field inc = () => { this.count++ } whose this is lexically the instance — but the type system is what surfaces the bug before it ships.

Why this works

Why does an arrow function fix it but a method doesn’t? A method’s this is dynamic — resolved from the call. An arrow function has no own this; it closes over the this of the enclosing scope at definition time. So a class field onClick = () => … captures the instance lexically and cannot be re-bound by a caller. The cost is one closure allocated per instance instead of one shared prototype method — usually negligible, occasionally relevant for objects created in tight loops.

this-based type guards and polymorphic this

Two this-typed features power real APIs. First, a method can return this is Derived — a this-based type guard that narrows the receiver itself in the calling scope:

class FileNode {
  isDir(): this is DirNode {   // narrows `this`, not a parameter
    return "children" in this;
  }
}
class DirNode extends FileNode {
  children: FileNode[] = [];
}

declare const node: FileNode;
if (node.isDir()) {
  node.children;
  //   ^? FileNode[]   — node narrowed to DirNode inside the branch
}

Second, polymorphic this makes a method’s return type “whatever the current class is”, which keeps fluent builders correctly typed across inheritance:

class Query {
  where(cond: string): this {   // returns the concrete subclass, not Query
    return this;
  }
}
class UserQuery extends Query {
  byEmail(e: string): this { return this; }
}

new UserQuery().where("active").byEmail("a@b.com");
//                                ^^^^^^^ ok — where() returned UserQuery, not Query

If where had returned Query instead of this, the chain would break at .byEmail because Query has no such method. Polymorphic this is the type-level mechanism behind chainable builders. ThisType<T> is the related contextual-typing marker the standard library uses to type this inside object-literal method bags (the Vue-2 options object, mixins, fluent config), so methods inside the literal see the full assembled object as their this.

Quiz

Given `const f = obj.method; f();` where `method` is typed with `this: Obj`, why does TypeScript flag the call to `f()`?

Complete the analogy

A method's `this` is like a hotel key card that only works while you are checked into a specific room. Carry the bare card to a different building and the system, by analogy, treats it as belonging to ___ — which is why a detached method loses its original receiver.

Recall before you leave
  1. 01
    Explain the lost-`this` problem end to end: how TypeScript types `this`, why detaching a method breaks it, and the two standard fixes.
  2. 02
    Compare a this-based type guard, polymorphic `this`, and ThisType<T> — what does each model?
Recap

TypeScript types the call-time receiver with a fake first parameter this: T that is erased at emit and exists only to check the receiver at each call site; --noImplicitThis (in strict) forces you to declare it when it can’t be inferred. Methods infer this as their container, but binding is dynamic, so detaching a method loses this — the lost-this bug, which TypeScript catches under the this-parameter and strictBindCallApply checks, fixed with .bind or an arrow class field. For APIs, this is Derived narrows the receiver, polymorphic this types fluent builders across subclasses, and ThisType<T> types this inside object-literal method bags. Next we move from typing the receiver to changing control flow: assertion functions, the asserts x is T signatures that narrow the rest of the scope by throwing. Now when you see a crash where this.state is undefined inside an event handler, you know exactly what happened — and you know the two lines it takes to fix it before the bug ever ships.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.