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.
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 QueryIf 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.
Given `const f = obj.method; f();` where `method` is typed with `this: Obj`, why does TypeScript flag the call to `f()`?
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.
- 01Explain the lost-`this` problem end to end: how TypeScript types `this`, why detaching a method breaks it, and the two standard fixes.
- 02Compare a this-based type guard, polymorphic `this`, and ThisType<T> — what does each model?
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.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.