Skip to main content

TS Ep 24: The Polymorphic `this` Type & Fluent APIs

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 24: This Article
In Object-Oriented Programming, Fluent Interfaces allow chaining multiple methods together sequentially (builder.setHost("localhost").setPort(8080).build()). TypeScript’s polymorphic this type ensures that method chaining preserves the exact subclass type across complex inheritance hierarchies.

1. Method Chaining & The Subclass Return Type Loss Problem
#

Consider a standard base class QueryBuilder that provides method chaining by executing return this:

class QueryBuilder {
  protected table: string = "";
  protected conditions: string[] = [];

  public from(table: string): this {
    this.table = table;
    return this;
  }

  public where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }
}

Now, suppose we extend QueryBuilder to create a specialized PostgresQueryBuilder with an added .returning() method:

class PostgresQueryBuilder extends QueryBuilder {
  protected returningFields: string[] = [];

  public returning(field: string): this {
    this.returningFields.push(field);
    return this;
  }

  public buildSql(): string {
    let sql = `SELECT * FROM ${this.table}`;
    if (this.conditions.length > 0) {
      sql += ` WHERE ${this.conditions.join(" AND ")}`;
    }
    if (this.returningFields.length > 0) {
      sql += ` RETURNING ${this.returningFields.join(", ")}`;
    }
    return sql;
  }
}

What Happens Without Polymorphic this?
#

If the base class methods (from, where) returned explicit : QueryBuilder instead of : this:

// If Base returned QueryBuilder:
const q = new PostgresQueryBuilder();

q.from("users") // Returns QueryBuilder!
  .where("age > 18") // Returns QueryBuilder!
  // ❌ Compiler Error: Property 'returning' does not exist on type 'QueryBuilder'!
  // .returning("id") 

Because from() was defined on the base class, returning : QueryBuilder causes TypeScript to forget that the object is actually an instance of PostgresQueryBuilder!


2. The Solution: The Polymorphic this Return Type
#

In TypeScript, annotating a method’s return type as : this creates a dynamic, subtype-aware type annotation.

The this return type dynamically resolves to whatever derived class is currently calling the method, preserving the subclass’s exact type identity throughout the chain!

const pgQuery = new PostgresQueryBuilder()
  .from("orders")          // Inferred Return Type: PostgresQueryBuilder
  .where("total > 100")    // Inferred Return Type: PostgresQueryBuilder
  .returning("order_id")   // 🟢 WORKED! 'returning' is recognized flawlessly!
  .buildSql();

console.log(pgQuery);
// Output: "SELECT * FROM orders WHERE total > 100 RETURNING order_id"

3. Building a Production Fluent Builder API
#

The polymorphic this type is the core mechanic powering popular libraries like TypeORM, Knex, Zod, and Express.

Let’s build a production-grade HTTP Request Builder using polymorphic this:

class HttpRequestBuilder {
  protected url: string = "";
  protected method: "GET" | "POST" | "PUT" | "DELETE" = "GET";
  protected headers: Record<string, string> = {};

  public setUrl(url: string): this {
    this.url = url;
    return this;
  }

  public setMethod(method: "GET" | "POST" | "PUT" | "DELETE"): this {
    this.method = method;
    return this;
  }

  public setHeader(key: string, value: string): this {
    this.headers[key] = value;
    return this;
  }
}

class AuthenticatedRequestBuilder extends HttpRequestBuilder {
  public setBearerToken(token: string): this {
    this.setHeader("Authorization", `Bearer ${token}`);
    return this;
  }

  public send(): void {
    console.log(`[HTTP ${this.method}] -> ${this.url}`);
    console.log(`[Headers]:`, JSON.stringify(this.headers));
  }
}

// Fluent Method Chaining
new AuthenticatedRequestBuilder()
  .setUrl("https://api.acme.com/v1/profile")
  .setMethod("GET")
  .setBearerToken("secret_jwt_token_12345") // Defined on subclass!
  .setHeader("Accept", "application/json")   // Inherited from base class!
  .send();

4. Explicit this Parameter Annotations
#

In JavaScript, the value of this inside a function depends on how the function is invoked at runtime (e.g. calling a method as an isolated callback unbinds this).

TypeScript allows declaring an explicit this parameter as the first argument of a function signature. This is a compile-time directive that tells TS what this must be when calling the function:

interface UserUI {
  name: string;
  // Declare that this function MUST be invoked with a 'UserUI' context for 'this'!
  renderName(this: UserUI): void;
}

const userUI: UserUI = {
  name: "Sarah Connor",
  renderName() {
    console.log(`User: ${this.name}`);
  },
};

userUI.renderName(); // 🟢 Valid!

const detachedRender = userUI.renderName;

// ❌ Compiler Error: The 'this' context of type 'void' is not assignable 
// to method's 'this' of type 'UserUI'.
// detachedRender(); 

Module 2 Completion Milestone! 🎉
#

Congratulations! You have completed Module 2: Object-Oriented TypeScript (Classes & OOP)!

Throughout these 9 episodes (Ep 16-24), you have mastered:

  • Class field declarations, readonly instance fields, and strictPropertyInitialization.
  • 4 strategies for handling uninitialized properties (default, null, ?, !).
  • The Visibility Matrix: public, protected, private, and ES2022 #privateFields.
  • Parameter Properties shorthand (constructor(public name: string)).
  • Getters, setters, backing fields (_field), and Asymmetric Accessor Types.
  • Interface implementation contracts (implements) and the Dependency Inversion Principle.
  • Class inheritance (extends), constructor chaining (super()), and the override keyword.
  • Non-instantiable Abstract Classes and the Template Method Design Pattern.
  • Static fields, public/private static, static blocks (static {}), and the Singleton Pattern.
  • The Polymorphic this Return Type and explicit this parameter annotations for Fluent APIs.

In the next module, Module 3: Intermediate TypeScript — Type Transformations & Generics, we will explore Type Aliases vs Interfaces, keyof, typeof, Generics, and TypeScript’s built-in Utility Types!

typescript - This article is part of a series.
Part 24: This Article