In TypeScript, a base class declares `where(sql: string): QueryBuilder` and `class UserQuery extends QueryBuilder` adds `withRoles()`. Why does `new UserQuery().where('id = 1').withRoles()` fail to compile, and what single change fixes it?
answer
- the annotation, not the runtime, loses it
- chain narrows to the base class
- an implicit type parameter per class
- resolved at each call site
- one signature, accurate for every subclass
basics
~20 sThe annotation is the bug. where is declared to return QueryBuilder, so the chain collapses to the base type and withRoles is not on it. Declare the return type as this, the polymorphic this type, and the subclass survives the chain.
solid answer
~40 sThe problem is the hand-written return type. `where` says it returns `QueryBuilder`, so the moment you call it the expression's type is the base class, and `withRoles` — declared only on `UserQuery` — is not a member of it. The compiler is being accurate: nothing in that signature promises the subclass comes back. Change the annotation to `where(sql: string): this` and it does. The polymorphic `this` type behaves like an implicit type parameter bounded by the declaring class, instantiated with the type of the receiver, so on a `UserQuery` the call evaluates to `UserQuery` and the chain keeps every subclass member. The same works in interfaces and abstract declarations, and it changes nothing about the emitted JavaScript — the type layer is erased either way.
code
typescript · 21 linesclass QueryBuilder {
private parts: string[] = [];
where(sql: string): this {
this.parts.push(`WHERE ${sql}`);
return this;
}
build(): string {
return this.parts.join(' ');
}
}
class UserQuery extends QueryBuilder {
withRoles(): this {
return this.where('role IS NOT NULL');
}
}
const sql = new UserQuery().where('id = 1').withRoles().build();
console.log(sql);go deeper
Recognise the error message shape: the chained expression is typed as the base class, so a subclass-only method is not found. Know that annotating the return type as this is the fix.
Explain that this is an implicit type parameter bounded by the declaring class and instantiated with the receiver's type, and that a body of return this would have inferred it without any annotation.
Demonstrate the assignability asymmetry and its consequence: this is assignable to the class but not the reverse, which is why a method typed this cannot hand back a freshly built base instance.
Own the library-design angle: publishing base-class return types in a hierarchy meant for extension silently breaks every downstream subclass's chain, and fixing it later is a signature change across the public surface.
## Where the chain collapses ```ts class QueryBuilder { where(sql: string): QueryBuilder { /* ... */ return this; } } class UserQuery extends QueryBuilder { withRoles(): UserQuery { return this; } } new UserQuery().where('id = 1').withRoles(); // error: Property 'withRoles' does not exist on type 'QueryBuilder'. ``` At runtime this works perfectly — `where` returns the same `UserQuery` object, which certainly has `withRoles` on its prototype chain. The failure is a modelling failure: the declared signature says `QueryBuilder`, and the checker reasons from the declaration, not from what the body happens to do. Once the expression's type is `QueryBuilder`, every subclass member is invisible for the rest of the chain, no matter what order the calls come in. ## `this` in type position TypeScript lets you write `this` where a type is expected. That is the **polymorphic `this` type**, and it is a different thing from the `this` value inside the body. Think of it as an implicit type parameter that every class declaration carries, constrained by the class itself, and instantiated with the type of the receiver at each call site: ```ts class QueryBuilder { private parts: string[] = []; where(sql: string): this { this.parts.push(`WHERE ${sql}`); return this; } } ``` Called on a `QueryBuilder`, `where` returns `QueryBuilder`. Called on a `UserQuery`, the very same declaration returns `UserQuery`. One signature, inherited unchanged, that stays accurate for every subclass anyone writes later — including subclasses that did not exist when the base was written. ## Walking the fixed example ```ts class UserQuery extends QueryBuilder { withRoles(): this { return this.where('role IS NOT NULL'); } } new UserQuery().where('id = 1').withRoles(); // ok ``` `new UserQuery()` has type `UserQuery`; `where` therefore yields `UserQuery`; `withRoles` is found. Note the subclass method also returns `this` rather than `UserQuery` — that keeps the chain intact for anyone who subclasses `UserQuery` in turn. And note that `this.where(...)` inside `withRoles` type-checks: it returns `this`, which is exactly what the signature promises. ## Assignability runs one way Because `this` is bounded by the declaring class, `this` is assignable to `QueryBuilder` — a `this` value is always at least a `QueryBuilder`. The reverse is not true: a plain `QueryBuilder` is **not** assignable to `this`, since `this` might stand for some subclass with extra members. That asymmetry is the whole reason the type is useful, and it is also why a method typed `(): this` cannot simply hand back a freshly constructed base instance. ## You usually get it for free If the body is just `return this;` and you leave the return type off, the compiler infers `this` on its own. In real code the collapsed chain almost always traces back to someone helpfully writing the class name by hand, or to a `.d.ts` written that way. Reviewing a fluent class, the question to ask is not "is the return type annotated?" but "does the annotation say `this`?" ## Interfaces and abstract members The polymorphic `this` type is not limited to class bodies: ```ts interface Resettable { reset(): this; } class Session implements Resettable { reset(): this { return this; } } ``` Each implementer instantiates `this` with its own type, so an interface can describe a fluent contract without knowing any of the classes that will satisfy it. ## The promise it makes `(): this` claims the returned value *is* the receiver's own type. That holds trivially when the body returns `this`. It does not hold for a method that builds a new object, and the compiler will say so — a separate problem worth knowing about before you reach for `this` on a copy-on-write API. ## Erasure Swapping `QueryBuilder` for `this` in the annotation changes the emitted JavaScript not at all. There is no runtime marker, no helper, no reflection: the subclass came back either way, and the only difference is whether the checker was willing to admit it.
- Which way does assignability run between `this` and the declaring class type?`this` is assignable to the class type, because it is bounded by it — a `this` value is always at least a `QueryBuilder`. The reverse fails: a plain `QueryBuilder` is not assignable to `this`, since `this` could stand for a subclass with additional members that the base instance lacks.
- Does switching the annotation from the class name to `this` change the emitted JavaScript?No. Both compile to the same `return this;`. The runtime always handed back the subclass instance; only the checker's view differed. This is the erasure point interviewers are usually fishing for — the fix is entirely a compile-time correction.
- Can an interface declare a fluent method that preserves the implementing type?Yes — `interface Resettable { reset(): this; }`. Each implementing class instantiates `this` with its own type, so a class implementing it can chain `reset()` and still see its own members. Abstract members work the same way; both need the annotation written out because there is no body to infer from.
saying these in an interview costs you the question
- Says the runtime loses the subclass, so a cast is needed
- Claims each chained call constructs a new base instance
- Thinks the base class type is assignable to this
- Believes overriding the method in every subclass is the only fix
- Assumes the failure depends on a strictness compiler flag