skip to content

In TypeScript, `class Base { clone(): this { return new Base(); } }` does not compile — the returned Base is not assignable to `this`. Why is the compiler right, and what are the realistic ways to type a clone or copy-on-write method?

level: seniorimportance: should knowfreq 24%

answer

  1. this is an implicit type parameter
  2. the caller chooses the type, not the body
  3. a subclass could add required members
  4. clone from the receiver's own prototype
  5. assertion needed — unchecked either way

basics

~20 s

The compiler is right: this stands for the receiver's actual class, which may be a subclass with extra members, so a freshly constructed Base does not satisfy it. Return this only when you genuinely hand back the receiver; a constructed copy needs an explicit assertion.

solid answer

~50 s

`this` behaves like an implicit type parameter bounded by `Base`, so a method typed `(): this` promises to return an instance of whatever subclass it was called on — and `new Base()` is not that. If `class Sub extends Base { extra = 1 }` exists, `const s: Sub = new Sub().clone()` would hand back a bare `Base` and `s.extra` would be undefined; the error prevents exactly that. Realistic options: return the receiver itself, which is the only body that satisfies `this` with no help; build the copy from the receiver's own prototype and assert it, as in `Object.create(Object.getPrototypeOf(this)) as this` — unchecked, and it skips constructors; or drop the polymorphic type, declare the return as `Base`, and let subclasses override with a narrower type. The honest summary is that `this` describes mutate-and-return APIs, not copy-on-write ones.

code

typescript · 23 lines
typescript
class Base {
  constructor(public label = '') {}

  // Mutate-and-return: satisfies `this` with no assertion.
  withLabel(label: string): this {
    this.label = label;
    return this;
  }

  // Copy: built from the receiver's real prototype, then asserted.
  clone(): this {
    const copy = Object.create(Object.getPrototypeOf(this)) as this;
    Object.assign(copy, this);
    return copy;
  }
}

class Sub extends Base {
  extra = 1;
}

const s: Sub = new Sub().withLabel('x').clone();
console.log(s instanceof Sub, s.extra, s.label);

go deeper

for a junior

Know that a method returning this must actually hand back the object it was called on; constructing a new instance of the class is not the same thing and will not compile.

for a middle

Explain the error by reading this as a type parameter bounded by the class: the caller picks the type, so a body that constructs one specific class cannot satisfy it.

for a senior

Show the concrete failure you are being protected from, and walk the options — return the receiver, clone from its prototype with an assertion, or drop to concrete return types with overrides — naming what each costs.

for a principal

Own the API decision behind it: this-typed methods describe mutate-in-place APIs, so choosing an immutable copy-on-write surface means giving up free subclass preservation across the whole public hierarchy.

## Why the compiler refuses The polymorphic `this` type is best modelled as an implicit type parameter that every class carries, constrained by the class itself. Reading `clone(): this` as `clone<T extends Base>(): T` makes the error obvious: the method body has to produce a value of a type chosen by the *caller*, and `new Base()` is one specific type the body picked. The compiler's message says as much — `Base` satisfies the constraint, but `this` could be instantiated with a different subtype of `Base`. ## The failure it is preventing ```ts class Base { clone(): this { return new Base(); } // error } class Sub extends Base { extra = 1; } const s: Sub = new Sub().clone(); // would be typed Sub… s.extra.toFixed(); // …but is a bare Base at runtime ``` Without the error, `clone()` on a `Sub` would be typed `Sub` and be a `Base`. Every subclass member the caller then touches is `undefined`, and the crash lands far from the class that caused it. This is not the compiler being pedantic; it is the compiler refusing to certify a claim the body cannot honour. ## Option 1 — genuinely return the receiver ```ts withLabel(label: string): this { this.label = label; return this; } ``` `this` is trivially of type `this`, so a mutate-and-return fluent method needs nothing extra. This is the shape the polymorphic `this` type was designed for, and if your method fits it, there is no problem to solve. ## Option 2 — build from the receiver's prototype and assert If you must produce a new object, derive it from the receiver so that at runtime it really is the right class, then tell the compiler what you did: ```ts clone(): this { const copy = Object.create(Object.getPrototypeOf(this)) as this; Object.assign(copy, this); return copy; } ``` The runtime part is sound — the copy's prototype is the receiver's actual prototype, so a `Sub` clones to a `Sub`. The `as this` is an assertion: unverified, and it is your responsibility that the object is complete. This route also bypasses constructors entirely, so any invariant a subclass constructor establishes is skipped, and `#private` fields are not carried across by `Object.assign`. The constructor route has the same shape. `this.constructor` is typed as `Function`, which carries no construct signature, so you cannot call `new this.constructor()` without asserting it to a construct-signature type — and that assertion also lies about the constructor's parameters, which subclasses are free to change. ## Option 3 — do not claim the polymorphic type ```ts class Base { clone(): Base { return new Base(); } } class Sub extends Base { override clone(): Sub { return new Sub(); } } ``` This compiles with no assertion anywhere, because each class states what it actually returns. The cost is that every subclass must remember to override `clone`, and one that forgets silently downgrades its callers to `Base` — the same collapse a fluent chain suffers, but now correctly reported rather than hidden. ## Option 4 — push the obligation onto subclasses Declaring `abstract clone(): this` on an abstract base moves the problem rather than solving it: a subclass implementing `return new Sub();` hits exactly the same error, because `Sub` itself could be extended further. Only a class nobody extends escapes, and TypeScript has no `final` to enforce that. ## The design lesson The error is telling you something about your API, not just your annotation. `(): this` is the right type for a mutate-in-place fluent interface. A copy-on-write builder — where each call returns a fresh instance — cannot be typed with `this` honestly, and a codebase that wants both usually ends up choosing: keep mutation and enjoy free subclass-preserving chaining, or go immutable and accept concrete return types plus overrides. Deciding that deliberately is far better than discovering it through an assertion someone added to silence the checker. ## Erasure, as always Every option above compiles to the same category of JavaScript — object creation and assignment. `as this` emits nothing and checks nothing; it changes only what the checker will let downstream code do. That is the point to make explicit if an interviewer asks whether the assertion is "safe": it is safe exactly to the degree that your reasoning about the runtime object was correct.

  • Why can you not just write `return new this.constructor();` and be done with it?
    `this.constructor` is typed as `Function`, which declares no construct signature, so the call is rejected until you assert it to something like `new () => this`. That assertion is unchecked and also fixes a parameter list subclasses are free to change, so it trades one unsound spot for another.
  • Using `this` as a *parameter* type — say `equals(other: this)` — is allowed. Is it sound?
    No. Viewed through a base-typed reference, the parameter resolves to the base type, so a caller may pass a plain base instance to an object that is really a subclass whose override expects subclass members. It is a known unsound spot, and the usual fix is to take the base type explicitly and narrow inside.
  • Does declaring the method `abstract clone(): this` on an abstract base fix anything?
    It relocates the error. A concrete subclass implementing `return new Sub();` fails for the same reason — `Sub` may itself be extended. Only a class nobody extends is safe, and TypeScript has no `final` keyword to guarantee that, so an assertion or a concrete return type is still where you end up.

saying these in an interview costs you the question

  • Says the error is a compiler bug and reaches for any
  • Thinks new Base() satisfies this because Base is the declaring class
  • Believes marking the method abstract removes the error
  • Assumes as this makes the copy verified at runtime
  • Claims Object.assign carries over #private fields

context