TypeScript 82 🔷 The this Type and Polymorphic this
The this type in TypeScript is not just a runtime reference. It is a type with its own semantics, and in class hierarchies it has a special polymorphic form that unlocks fluent APIs and accurate subclass inference. The polymorphic this type represents a type that is the subtype of the containing class or interface — a concept formally known as F-bounded polymorphism . When a method returns this instead of the class name, TypeScript understands that the actual return type is the type of the instance on which the method was called, not the type of the class that declared the method .
This distinction is what makes method chaining work correctly across inheritance. A base class method that returns Animal breaks the chain when called on a Cat because the compiler forgets that the receiver was a Cat. A method that returns this preserves the receiver’s type through the chain, so subclass-specific methods remain available .
Key point: The this type is a subtype of the containing class’s instance type, but not vice versa. This is because this might actually be a subclass instance at runtime . When you write return this, the compiler infers the polymorphic type, and when you write : this explicitly, you tell the compiler to preserve the receiver type through the method call. This is the mechanism behind fluent interfaces and builder patterns in TypeScript.
Why the this type exists
The this type solves a specific problem that ordinary class types cannot: preserving the identity of the receiver through method calls.
The method chaining problem. Consider a base class with an eat() method that returns the class type. When you call cat.eat(), the return type is Animal, not Cat. You cannot then call cat.eat().meow() because Animal does not have a meow method. The chain breaks. The this type fixes this by making eat() return the type of whatever instance called it — Cat when called on a Cat, Animal when called on an Animal .
The builder pattern problem. A builder class with methods like setName(), setAge(), and build() needs each setter to return the builder so calls can be chained. If the builder is subclassed — say AdvancedBuilder extends Builder — the subclass’s setters must return AdvancedBuilder, not Builder, or the chain loses access to the subclass-specific methods. The this type handles this automatically .
The clone pattern problem. An interface with a clone() method needs to return the type of the implementing class. Writing clone(): Cloneable loses type information. Writing clone(): this in an interface works but has known limitations when implemented in classes — casting is sometimes required .
The generic constraint problem. Before polymorphic this, expressing “this method returns the same type as the receiver” required the F-bounded generic pattern: interface Cloneable<T extends Cloneable<T>> { clone(): T }. The this type replaces that ceremony with a single keyword .
The trade-off. The this type is powerful but has edge cases. It works best in classes with implementations. In interfaces, it can be difficult to implement without casts . It also does not extend to structural typing the way some other type features do — it is tied to the nominal class hierarchy .
a. The this Type in Methods and Return Positions
The this type is available in non-static members of classes and interfaces. It refers to the type of the instance on which the method is called. When a method returns this, the return type is polymorphic: it changes based on the receiver.
class Car {
Rent(type: string): this {
console.log(`${type} has been rented.`);
return this;
}
Record(): this {
console.log(`Car was rented at ${new Date().toLocaleString()}`);
return this;
}
Return(type: string): this {
console.log(`${type} has been returned.`);
return this;
}
}
class ElectricCar extends Car {
Charge(): this {
console.log(`Electric car has been charged.`);
return this;
}
}
class GasCar extends Car {
Refill(): this {
console.log(`Gas car has been refilled.`);
return this;
}
}
With these definitions, the chains behave correctly:
const electricCar = new ElectricCar();
electricCar
.Rent("Electric car")
.Record()
.Charge(); // ✅ Charge is available because this is ElectricCar
const gasCar = new GasCar();
gasCar
.Rent("Gas car")
.Record()
.Refill(); // ✅ Refill is available because this is GasCar
The Record() method is declared in Car and returns this. When called on an ElectricCar instance, this resolves to ElectricCar. The chain continues to Charge(), which exists only on ElectricCar. Without the polymorphic this type, Record() would return Car, and Charge() would be a compile error .
The this type is not limited to return positions. It can appear in parameter types and indexed access:
class Foo {
axisControl: string = "string";
directionControl: string = "string";
filteredAxis: string[] = ["string"];
filteredDirection: string[] = ["string"];
test(filteredName: `filtered${string}` & keyof this) {
this[filteredName] = ["otherString"];
}
}
However, this pattern has known limitations. The compiler does not always resolve this[filteredName] to the expected type, and the error messages can be opaque . For most practical uses, the this type is most valuable in return positions for method chaining.
b. When to Use this vs the Class Name
The choice between returning this and returning the class name is not stylistic. It determines whether subclasses can extend fluent APIs.
// ❌ Breaks in subclasses
class Animal {
eat(): Animal {
console.log("I'm moving!");
return this;
}
}
class Cat extends Animal {
meow(): Cat {
console.log('Meow~');
return this;
}
}
const cat = new Cat();
cat.eat().meow(); // Error: Property 'meow' does not exist on type 'Animal'
The error occurs because eat() returns Animal, and Animal does not have a meow method. Even though cat is a Cat, the return type of eat() is the declared class name, not the receiver type. TypeScript uses the declared return type, not runtime knowledge .
// ✅ Preserves subclass type
class Animal {
eat(): this {
console.log("I'm moving!");
return this;
}
}
class Cat extends Animal {
meow(): Cat {
console.log('Meow~');
return this;
}
}
const cat = new Cat();
cat.eat().meow(); // ✅ Works — eat() returns Cat
With : this, the return type is the type of the receiver. When called on a Cat, eat() returns Cat, and meow() is available .
The rule is straightforward: if a method returns this and its body returns this, declare the return type as this. If the method returns a new object or a different value, use the appropriate type. The @typescript-eslint/prefer-return-this-type rule enforces this pattern automatically .
There is one exception: if the method is declared in a base class and intentionally returns the base type to hide subclass-specific APIs, returning the class name is correct. This is a deliberate design choice, not an accident.
c. Polymorphic this in Interfaces and Generic Constraints
The this type is also available in interfaces, though with caveats. An interface method can declare clone(): this, and implementing classes can return this. However, TypeScript’s handling of this in interface implementations has known issues that sometimes require casts .
interface ICloneable {
clone(): this;
}
class A implements ICloneable {
constructor(readonly a: number) {}
clone(): this {
return new A(this.a) as this; // cast required
}
}
The cast as this is necessary because TypeScript cannot verify that new A(this.a) is assignable to this — this might be a subclass instance, and A is the base. The runtime behavior is correct, but the compiler needs the assertion .
For generic contexts, polymorphic this replaces the F-bounded pattern. Instead of:
interface Cloneable<T extends Cloneable<T>> {
clone(): T;
}
You write:
interface Cloneable {
clone(): this;
}
The second form is cleaner and does not require the implementing class to specify itself as a type argument . The trade-off is that the interface form has implementation limitations that the class form does not.
Complete Example Session
This session builds a fluent query builder using the this type, then demonstrates the difference between this and class-name returns.
// ============================================
// PART 1: THE BASE QUERY BUILDER
// ============================================
class QueryBuilder {
protected table: string = "";
protected conditions: string[] = [];
from(table: string): this {
this.table = table;
return this;
}
where(condition: string): this {
this.conditions.push(condition);
return this;
}
build(): string {
const whereClause = this.conditions.length
? ` WHERE ${this.conditions.join(" AND ")}`
: "";
return `SELECT * FROM ${this.table}${whereClause}`;
}
}
// ============================================
// PART 2: THE SUBCLASS WITH EXTRA METHODS
// ============================================
class PostgresQueryBuilder extends QueryBuilder {
protected limitValue: number | null = null;
limit(n: number): this {
this.limitValue = n;
return this;
}
build(): string {
const base = super.build();
return this.limitValue ? `${base} LIMIT ${this.limitValue}` : base;
}
}
// ============================================
// PART 3: THE CHAIN WORKS ACROSS INHERITANCE
// ============================================
const query = new PostgresQueryBuilder()
.from("users")
.where("age > 18")
.limit(10)
.build();
console.log(query);
// SELECT * FROM users WHERE age > 18 LIMIT 10
// Without `this`, .limit() would fail because
// .where() would return QueryBuilder, not PostgresQueryBuilder.
// ============================================
// PART 4: THE CLASS NAME RETURN BREAKS THE CHAIN
// ============================================
class BadQueryBuilder {
protected table: string = "";
from(table: string): BadQueryBuilder {
this.table = table;
return this;
}
build(): string {
return `SELECT * FROM ${this.table}`;
}
}
class BadPostgresBuilder extends BadQueryBuilder {
protected limitValue: number | null = null;
limit(n: number): BadPostgresBuilder {
this.limitValue = n;
return this;
}
build(): string {
const base = super.build();
return this.limitValue ? `${base} LIMIT ${this.limitValue}` : base;
}
}
const bad = new BadPostgresBuilder();
// bad.from("users").limit(10); // ❌ Error: Property 'limit' does not exist on type 'BadQueryBuilder'
// from() returns BadQueryBuilder, losing the subclass type.
// ============================================
// PART 5: THE CLONE PATTERN WITH this
// ============================================
class Config {
constructor(public readonly name: string, public readonly value: string) {}
clone(): this {
return new Config(this.name, this.value) as this;
}
}
class SecureConfig extends Config {
constructor(name: string, value: string, public readonly encrypted: boolean) {
super(name, value);
}
clone(): this {
return new SecureConfig(this.name, this.value, this.encrypted) as this;
}
}
const secure = new SecureConfig("api", "secret", true);
const copy = secure.clone();
console.log(copy.encrypted); // true — type preserved
// ============================================
// PART 6: THE INTERFACE LIMITATION
// ============================================
interface ICloneable {
clone(): this;
}
class Data implements ICloneable {
constructor(public readonly id: number) {}
clone(): this {
// Cast required because TypeScript cannot prove
// new Data(...) is assignable to `this`.
return new Data(this.id) as this;
}
}
// ============================================
// PART 7: THE GENERIC ALTERNATIVE
// ============================================
// Before polymorphic this:
interface CloneableOld<T extends CloneableOld<T>> {
clone(): T;
}
class OldData implements CloneableOld<OldData> {
constructor(public readonly id: number) {}
clone(): OldData {
return new OldData(this.id);
}
}
// With polymorphic this, the type parameter is unnecessary.
// The trade-off is the cast in the implementation.
// ============================================
// PART 8: THE BUILDER PATTERN WITH MIXINS
// ============================================
type Constructor = new (...args: any[]) => {};
function WithLogging<TBase extends Constructor>(Base: TBase) {
return class extends Base {
log(message: string): this {
console.log(message);
return this;
}
};
}
class Service {
start(): this {
console.log("Service started");
return this;
}
}
const LoggableService = WithLogging(Service);
const svc = new LoggableService();
svc.start().log("Running"); // ✅ Chain works
// ============================================
// PART 9: THE FLUENT API WITH CONDITIONAL METHODS
// ============================================
class HttpClient {
private baseUrl: string = "";
private headers: Record<string, string> = {};
withBaseUrl(url: string): this {
this.baseUrl = url;
return this;
}
withHeader(key: string, value: string): this {
this.headers[key] = value;
return this;
}
get(path: string): string {
return `GET ${this.baseUrl}${path}`;
}
}
class AuthenticatedHttpClient extends HttpClient {
private token: string = "";
withToken(token: string): this {
this.token = token;
return this;
}
get(path: string): string {
const base = super.get(path);
return this.token ? `${base} [Auth: ${this.token}]` : base;
}
}
const client = new AuthenticatedHttpClient()
.withBaseUrl("https://api.example.com")
.withHeader("Accept", "application/json")
.withToken("secret")
.get("/users"); // ✅ withToken available after withHeader
// ============================================
// PART 10: THE TYPE INFERENCE BEHAVIOR
// ============================================
class Fluent {
step(): this {
return this;
}
}
class SubFluent extends Fluent {
subStep(): this {
return this;
}
}
const sub = new SubFluent();
const result = sub.step(); // type is SubFluent, not Fluent
console.log(result); // SubFluent instance
The ten parts cover the base query builder, the subclass with extra methods, the working chain, the broken chain with class-name returns, the clone pattern, the interface limitation, the generic alternative, mixins with this, the fluent HTTP client, and type inference behavior.
Quick Reference
The this Type Rules
| Rule | Description |
|---|---|
| Available in | Non-static members of classes and interfaces |
| Meaning | The type of the receiver (the instance calling the method) |
| Return type | When declared as : this, preserves the receiver type |
| Assignability | this is assignable to the class type, but not vice versa |
| In subclasses | this resolves to the subclass when called on a subclass instance |
this vs Class Name
| Scenario | Return Type | Result |
|---|---|---|
| Method chaining across inheritance | : this | Subclass methods remain available |
| Method chaining within one class | : ClassName or : this | Both work, : this is safer |
| Returning a new instance | : ClassName | Correct — this would be wrong |
| Interface implementation | : this | May require as this cast |
The F-Bounded Alternative
| Pattern | Syntax |
|---|---|
| Polymorphic this | clone(): this |
| F-bounded generic | interface C<T extends C<T>> { clone(): T } |
| Class implementation | clone(): this { return new X() as this } |
Best Practices
✅ Do This:
// Use this for methods that return the receiver
class Builder {
setName(name: string): this { // ✅
this.name = name;
return this;
}
}
// Use this in fluent APIs that support inheritance
class Animal {
eat(): this { // ✅
return this;
}
}
// Cast when implementing this in a class
clone(): this {
return new Config(...) as this; // ✅
}
// Let the compiler infer this when the body returns this
eat() { // ✅ implicit this return
return this;
}
❌ Don’t Do This:
// Don't return the class name for chaining methods
class Animal {
eat(): Animal { // ❌ breaks subclass chains
return this;
}
}
// Don't return this when you create a new object
clone(): this {
return { ...this }; // ❌ wrong — this is a class instance
}
// Don't use this in static members
class Foo {
static bar(): this { // ❌ error — not in static member
return this;
}
}
// Don't assume this works in object literals without capture
const obj = {
method(): this { // ❌ `this` refers to method, not obj
return this;
}
};
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Chain breaks in subclass | Return type is class name, not this | Change return type to : this |
as this cast required | Interface with this return implemented in class | Cast the return value |
this unavailable in static | Static members have no receiver instance | Use class name |
this resolves to wrong type | Used in object literal, not class/interface | Capture outer this or use arrow function |
Type error on this[key] | Indexed access with this is limited | Use explicit type or class name |
| Overriding method changes return | Subclass method returns narrower type | Return this for consistency |
Real-World Examples
1. Fluent Builder
class Builder {
setName(name: string): this { this.name = name; return this; }
build(): string { return this.name; }
}
2. Method Chaining with Subclass
class Car {
Rent(): this { return this; }
}
class ElectricCar extends Car {
Charge(): this { return this; }
}
new ElectricCar().Rent().Charge();
3. Clone Pattern
class Config {
clone(): this { return new Config(this.name) as this; }
}
4. Interface with this
interface ICloneable {
clone(): this;
}
5. F-Bounded Generic Replacement
// Before: interface C<T extends C<T>> { clone(): T }
// After: interface C { clone(): this }
6. HTTP Client Chain
new HttpClient()
.withBaseUrl("https://api.example.com")
.withHeader("Accept", "application/json")
.get("/users");
7. Database Query Builder
new PostgresQueryBuilder()
.from("users")
.where("age > 18")
.limit(10)
.build();
8. Mixin with this
function WithLogging<T extends Constructor>(Base: T) {
return class extends Base {
log(msg: string): this { console.log(msg); return this; }
};
}
9. Type Inference in Subclass
const sub = new SubFluent();
const result = sub.step(); // SubFluent
10. Conditional Fluent API
class AuthClient extends Client {
withToken(token: string): this { this.token = token; return this; }
}
Visual: Method Chaining With and Without this
┌──────────────────────────────────────────────┐
│ WITH CLASS NAME RETURN │
│ │
│ class Animal { │
│ eat(): Animal { return this; } │
│ } │
│ class Cat extends Animal { │
│ meow(): Cat { return this; } │
│ } │
│ │
│ cat.eat() → returns Animal │
│ │ │
│ ▼ │
│ .meow() → ❌ Error: Animal has no meow │
│ │
├──────────────────────────────────────────────┤
│ WITH this RETURN │
│ │
│ class Animal { │
│ eat(): this { return this; } │
│ } │
│ class Cat extends Animal { │
│ meow(): this { return this; } │
│ } │
│ │
│ cat.eat() → returns Cat │
│ │ │
│ ▼ │
│ .meow() → ✅ Works │
│ │
└──────────────────────────────────────────────┘
Visual: The this Type Hierarchy
┌──────────────────────────────────────────────┐
│ this TYPE HIERARCHY │
│ │
│ Animal (class) │
│ └─ this: subtype of Animal │
│ │
│ Cat extends Animal │
│ └─ this: subtype of Cat │
│ │
│ When method returns this: │
│ ├─ Called on Cat → returns Cat │
│ ├─ Called on Animal → returns Animal │
│ └─ Called on subclass → returns subclass │
│ │
│ this is assignable to Animal, │
│ but Animal is not assignable to this. │
│ (because this might be Cat) │
│ │
└──────────────────────────────────────────────┘
Visual: F-Bounded Polymorphism
┌──────────────────────────────────────────────┐
│ F-BOUNDED POLYMORPHISM │
│ │
│ Before this: │
│ interface Cloneable<T extends Cloneable<T>> │
│ clone(): T │
│ │
│ class A implements Cloneable<A> │
│ clone(): A │
│ │
│ With this: │
│ interface Cloneable │
│ clone(): this │
│ │
│ class A implements Cloneable │
│ clone(): this { return new A() as this } │
│ │
│ The this type replaces the recursive │
│ generic constraint. │
│ │
└──────────────────────────────────────────────┘
Visual: this in Interfaces vs Classes
┌──────────────────────────────────────────────┐
│ INTERFACE vs CLASS IMPLEMENTATION │
│ │
│ Interface: │
│ interface ICloneable { │
│ clone(): this; │
│ } │
│ └─ Implementation requires cast: │
│ return new X() as this; │
│ │
│ Class: │
│ class Cloneable { │
│ clone(): this { │
│ return this; // no cast needed │
│ } │
│ } │
│ └─ Works without cast │
│ │
│ Prefer class implementations when possible. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| this type meaning | The type of the receiver (calling instance) |
| Return type syntax | method(): this |
| Assignability | this → class type, not vice versa |
| Use case | Method chaining, fluent APIs, clone patterns |
| Subclass behavior | this resolves to the subclass instance type |
| Interface limitation | as this cast may be required |
| Generic alternative | F-bounded: interface C<T extends C<T>> |
| Best practice | Return this for chaining methods |
Key takeaways:
- The
thistype represents the receiver, not the declaring class. When a method returnsthis, the return type is the type of the instance on which the method was called. On aCatinstance,thisisCat; on anAnimalinstance,thisisAnimal. - Use
: thisfor method chaining that must survive inheritance. Returning the class name breaks the chain when a subclass calls an inherited method, because the compiler forgets the subclass type. Returningthispreserves it . - The
thistype is a subtype of the containing class. It is assignable to the class type, but the class type is not assignable tothis, becausethismight be a subclass instance . - F-bounded polymorphism is the formal name. The recursive generic pattern
interface C<T extends C<T>>is replaced byclone(): this. Thethistype is cleaner and requires less ceremony . - Interfaces with
thishave implementation caveats. When a class implements an interface method declared asclone(): this, TypeScript may require anas thiscast because it cannot prove the returned object is the correct subtype . - The
thistype works in return positions, parameter types, and indexed access. The return position is the most common and most valuable use case. Indexed access withthishas known limitations . - Use
thiswhen the method returns the receiver; use the class name when it returns a new object. Returningthisfor a method that constructs a new instance is a type error waiting to happen.
Remember: The this type is not just a runtime reference — it is a type-level tool for preserving the identity of the receiver through method calls. In fluent APIs, builders, and clone patterns, returning this instead of the class name is the difference between a chain that works in subclasses and one that breaks. The this type is the TypeScript mechanism for F-bounded polymorphism, and it exists to solve a problem that ordinary class types cannot: keeping the receiver’s type intact as calls pass through inherited methods.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!