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
thistype, called polymorphic because it changes to fit each subclass. - Type predicate
- A return type like
x is Tthat tells the checker a function narrows a value. - Receiver
- The object a method is called on,
thisinside 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.
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.
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
Fix the lost subclass
In the second example change
add(): Base2toadd(): this. Predict the Types panel, then addconsole.log(new Kid2().add().extra()).Clean, and it prints
extra. - 2
Return the wrong object
In the builder example change
addso it ends withreturn new Builder();. Read the error before scrolling: why is that not allowed with athisreturn type?A new
Builderis not necessarily the same class as the receiver, so it can't satisfythis. - 3
Add a third case
In the
this isexample add a classLinkEntry extends FileEntry { target = "/x"; }and a methodisLink(): this is LinkEntry. Add anelse ifbranch toshowthat prints the target.
Code & diagrams
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> worldThe same code with add(): this instead of add(): Base2 is clean. Try it.
Console
(nothing was printed)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 bytestoString() 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 functionBreak 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.
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.
- ▸
thistypes are implicit type parameters: each class has a hidden parameter constrained to the class itself, andthisis instantiated with the receiver's type at the call. That is why returningnew Builder()from a: thismethod fails: it satisfies the class type but not the hidden parameter. - ▸
A
this is Tguard is different from a standaloneisDirectory(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 withasserts 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
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 withreturn this;. What should the method's return type be? If you write the class name (add(part: string): Base), then a subclass that inheritsaddloses its own methods after the first call:new Kid2().add().extra()is errorTS2339: Property 'extra' does not exist on type 'Base2'(the example below names its classesBase2andKid2). - 2
The fix is the **polymorphic
thistype**: write: thisas the return type.thisis not a fixed class; it means "whatever class the call was made on". For anHtmlBuilder,add()returnsHtmlBuilder; for a plainBuilderit returnsBuilder. The word polymorphic means "takes many shapes": the same method signature yields a different, more specific type for each subclass. - 3
You can use
thisas a type for parameters and properties too (equals(other: this)), in non-static members only. Athis-typed parameter is strict: aDogcan only be compared with anotherDog, not just anyAnimal. Static members don't have athistype in this sense because there is no instance. - 4
A second feature is a **
this-based type guard**: a method whose return type isthis is T, for exampleisDirectory(): this is DirEntry. When it returnstrue, TypeScript narrows the object you called it on toDirEntryin that branch. It is a type predicate (x is T, Topic 4.7) aimed at the receiver. Inside theif,e.childrenis available. Theelse if (e.isFile())branch is a second, separate guard for the other class. - 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 exampleselectbeforefrom) return a different type from each step (the step builder pattern) instead ofthis. - 6
Careful with a trap: the
thistype is only as honest as your code. Writingadd(): this { return new Builder(); }is a compile error, because a newBuilderis not necessarily the receiver's subclass. TypeScript makes you returnthisitself (or cast it) so the promise holds. And a method that returnsthisin a chain makes the object mutable: every call changes it, so sharing a half-built object between two chains mixes them up. - 7
Methods returning
thisare also where you meetthisparameters (Topic 2.6): a function can declarefunction f(this: Button)to say whatthismust be when it is called. In classes,thistypes do that job automatically for you.
Explain it without notes
What is the polymorphic this type and why is it better than the class name for fluent methods?
How does a this is T return type work?
Why is add(): this { return new Builder(); } an error?
How would you make a builder that forces select before from?
Practice
Write class Counter { n = 0; inc(): this { ... } } and a subclass LoggingCounter with log(): this. Chain inc().log().inc() and print n.
Write a class Shape with a method isCircle(): this is Circle and two subclasses. Use it in an if to read radius.
Trade-offs
- ↔
: thisgives 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
thisand explain why it beats the class name.I can write a method with a
this is Treturn type and use it in anif.I can explain why
new Builder()cannot be returned from a: thismethod.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.