Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~20 min· topic 7 of 8

Topic 6.7

this Types and Fluent APIs

In one line

Inside a class, the type this means "the exact class of the object this method was called on, including subclasses". It lets a method return its own object so calls chain (a.add().tag().build()) without losing the subclass, and it lets methods narrow this with this is T.

Think of it like this

A coffee order at a counter. You tell the barista: "a latte, with oat milk, extra hot, with a lid". Each instruction hands you back the same cup, now with one more thing done, so you can keep talking. A fluent API works that way: every method returns the object itself so you can chain calls. If the barista gave you back a plain "drink" after each step, you could no longer ask for latte-only things such as latte art. The type this keeps the cup a latte.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Fluent API
An interface where methods return the object so calls can be chained.
Method chaining
Calling methods one after another on the result of the last call.
this type
The type that means "the class of the object this method was called on".
Polymorphic this
The this type, called polymorphic because it changes to fit each subclass.
Type predicate
A return type like x is T that tells the checker a function narrows a value.
Receiver
The object a method is called on, this inside the method.
Builder
An object that you configure step by step and then ask to produce the final result.

Step by step

01Chaining with return this

Builder.add pushes a part and returns this so the next call can follow. The return type is this, not Builder.

HtmlBuilder extends Builder adds tag. Because add returns this, new HtmlBuilder().add("hello") is still an HtmlBuilder, so .tag("b") is available straight after.

builder.tswhole filets
class Builder {
  private parts: string[] = [];

  add(part: string): this {
    this.parts.push(part);
    return this;
  }

  build() {
    return this.parts.join(" ");
  }
}

class HtmlBuilder extends Builder {
  tag(name: string): this {
    return this.add("<" + name + ">");
  }
}

new HtmlBuilder().add("hello").tag("b").add("world").build();

02What happens with the class name instead

If add is declared : Base2, the chain forgets the subclass. After .add() the checker only knows it has a Base2, so .extra() is an error.

this fixes it with no extra code: only the annotation changes.

What happens with the class name insteaddiagram
Rendering diagram…

03this is T: narrowing the receiver

A method can return this is DirEntry. After if (e.isDirectory()) the object is a DirEntry inside the block, so e.children type-checks. The method body decides how to answer; here it uses instanceof.

As with every type guard, if the body lies, the narrowing lies. TypeScript trusts the signature.

04Typed steps instead of this

When the order matters, return a different object type from each step (select() returns a NeedsFrom, whose only method is from()). That is a bigger design than this, but the compiler then forbids calling methods out of order.

Try it yourself

  1. 1

    Fix the lost subclass

    In the second example change add(): Base2 to add(): this. Predict the Types panel, then add console.log(new Kid2().add().extra()).

    Clean, and it prints extra.

  2. 2

    Return the wrong object

    In the builder example change add so it ends with return new Builder();. Read the error before scrolling: why is that not allowed with a this return type?

    A new Builder is not necessarily the same class as the receiver, so it can't satisfy this.

  3. 3

    Add a third case

    In the this is example add a class LinkEntry extends FileEntry { target = "/x"; } and a method isLink(): this is LinkEntry. Add an else if branch to show that prints the target.

Code & diagrams

A fluent builder that keeps the subclass

add() returns this, so after add(...) the object is still an HtmlBuilder and tag(...) is available. The ^? confirms it.

Console

(nothing was printed)

Expected console output

hello <b> world
Returning the class name loses the subclass

The same code with add(): this instead of add(): Base2 is clean. Try it.

Console

(nothing was printed)
Methods that narrow the object with this is

Inside each branch the checker treats e as the narrower class, so children and size are available without a cast.

Console

(nothing was printed)

Expected console output

/src has 2 children
/a.txt is 120 bytes
A query builder and a broken chain

toString() ends the chain: it returns a string, and a string has no select. The error is precise, and the program crashes at the same spot at runtime.

Console

(nothing was printed)

Expected console output

SELECT * FROM users WHERE age > 18
Uncaught TypeError: (intermediate value).from(...).toString(...).select is not a function

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

End a chain with a method that returns something else

In the Query example keep .toString().select("x") at the end of the last line.

terminal
$ Types panel
── what you'll see ──
index.ts(21,49): error TS2339: Property 'select' does not exist on type 'string'.

Myth vs fact

Myth

this as a type is the same as writing the class name.

Fact

The class name is fixed. this follows the receiver, so subclasses get their own type back.

Myth

A this is T method makes the object become a T.

Fact

It only tells the checker how to narrow in the branch. The body must really answer correctly.

Myth

Fluent APIs are always better.

Fact

They read well for configuration but they hide mutation and make debugging harder (one long expression). Plain functions or objects can be clearer.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    this types are implicit type parameters: each class has a hidden parameter constrained to the class itself, and this is instantiated with the receiver's type at the call. That is why returning new Builder() from a : this method fails: it satisfies the class type but not the hidden parameter.

  • ▸

    A this is T guard is different from a standalone isDirectory(e): e is DirEntry: the receiver is narrowed instead of an argument, and the narrowing applies to the variable written before the dot. It also composes with asserts this is T (Topic 4.7) for methods that throw.

  • ▸

    For ordered, impossible-to-misuse chains, a step builder returns a new object type per step. It is more code than this, but every illegal order becomes a compile error. Libraries such as query builders use this for the required clauses.

Remember this

  1. 1

    The problem this solves: Setting up an object step by step with separate statements is long, and a chained method that returns the wrong type loses the methods of a subclass. A method chain is new Query().select("*").from("users").where("age > 18"): each call returns an object to call the next method on. The simplest way to get chaining is to end each method with return this;. What should the method's return type be? If you write the class name (add(part: string): Base), then a subclass that inherits add loses its own methods after the first call: new Kid2().add().extra() is error TS2339: Property 'extra' does not exist on type 'Base2' (the example below names its classes Base2 and Kid2).

  2. 2

    The fix is the **polymorphic this type**: write : this as the return type. this is not a fixed class; it means "whatever class the call was made on". For an HtmlBuilder, add() returns HtmlBuilder; for a plain Builder it returns Builder. The word polymorphic means "takes many shapes": the same method signature yields a different, more specific type for each subclass.

  3. 3

    You can use this as a type for parameters and properties too (equals(other: this)), in non-static members only. A this-typed parameter is strict: a Dog can only be compared with another Dog, not just any Animal. Static members don't have a this type in this sense because there is no instance.

  4. 4

    A second feature is a **this-based type guard**: a method whose return type is this is T, for example isDirectory(): this is DirEntry. When it returns true, TypeScript narrows the object you called it on to DirEntry in that branch. It is a type predicate (x is T, Topic 4.7) aimed at the receiver. Inside the if, e.children is available. The else if (e.isFile()) branch is a second, separate guard for the other class.

  5. 5

    Fluent APIs are common: query builders, test assertions (expect(x).toBe(1)), URL and request builders, validation chains. They are pleasant to read, but each method must really return the same object (or the type is a lie), and the chain should not end up needing steps in a fixed order that the types can't express. To enforce an order (for example select before from) return a different type from each step (the step builder pattern) instead of this.

  6. 6

    Careful with a trap: the this type is only as honest as your code. Writing add(): this { return new Builder(); } is a compile error, because a new Builder is not necessarily the receiver's subclass. TypeScript makes you return this itself (or cast it) so the promise holds. And a method that returns this in a chain makes the object mutable: every call changes it, so sharing a half-built object between two chains mixes them up.

  7. 7

    Methods returning this are also where you meet this parameters (Topic 2.6): a function can declare function f(this: Button) to say what this must be when it is called. In classes, this types do that job automatically for you.

Explain it without notes

01

What is the polymorphic this type and why is it better than the class name for fluent methods?

02

How does a this is T return type work?

03

Why is add(): this { return new Builder(); } an error?

04

How would you make a builder that forces select before from?

Practice

01

Write class Counter { n = 0; inc(): this { ... } } and a subclass LoggingCounter with log(): this. Chain inc().log().inc() and print n.

02

Write a class Shape with a method isCircle(): this is Circle and two subclasses. Use it in an if to read radius.

Trade-offs

  • ↔

    : this gives fluent, subclass-friendly chains for free, but the object is mutated by every call and the order of calls is not checked.

  • ↔

    Step builders enforce order at compile time but need one small type per step.

Done when you can

  • I can write a chainable method that returns this and explain why it beats the class name.

  • I can write a method with a this is T return type and use it in an if.

  • I can explain why new Builder() cannot be returned from a : this method.

  • I can spot where a chain stops because a method returns something else.

  • I know a step builder is the way to enforce call order.