skip to content

In TypeScript, what does a method whose declared return type is `this is Foo` do at call sites, and when would you model a check that way instead of exporting a standalone `value is Foo` guard?

level: seniorimportance: nice to knowfreq 24%

answer

  1. a predicate about the receiver
  2. narrows the object, not a parameter
  3. state that belongs to the instance
  4. intersection or subclass on the right
  5. not the same as returning this

basics

~20 s

A method returning this is Foo is a type predicate on the receiver: when it returns true, the object you called it on is narrowed to Foo for the rest of the branch. Reach for it when the object owns the state being checked.

solid answer

~50 s

`this is Foo` is the receiver-flavoured type predicate. Declared on a method in a class or an interface, it means: when this call returns `true`, narrow the object it was called on. So `if (draft.isPublishable()) { draft.body.trim(); }` compiles because `draft` is narrowed inside the branch, and `if (this.isPublishable())` narrows `this` inside another method of the same class. The asserted type is usually an intersection that tightens a field — `this is Draft & { body: string }` — or a subclass in a hierarchy, as in `isFile(): this is FileRep`. Prefer it when the state being checked belongs to the object, so the check is discoverable on the instance and callers need no import; prefer a free `value is Foo` guard when the value arrives from outside as plain data, where there is no instance to hang a method on. Note it is unrelated to *returning* `this` for fluent chaining.

code

typescript · 20 lines
typescript
class Draft {
  constructor(public title: string, public body?: string) {}

  isPublishable(): this is Draft & { body: string } {
    return this.body !== undefined;
  }

  publish(): string {
    if (this.isPublishable()) {
      return this.body.toUpperCase(); // this narrowed here too
    }
    throw new Error("Draft has no body");
  }
}

const d = new Draft("Title", "Body");
if (d.isPublishable()) {
  console.log(d.body.trim()); // body: string
}
console.log(d.publish());

go deeper

for a junior

Recognise the syntax when you meet it in a library: a method typed this is Something still returns true or false, and the object it was called on becomes more specific inside the if.

for a middle

Explain that the predicate's subject is the receiver rather than a parameter, and show both shapes — intersecting the class with tighter fields, or asserting a subclass — plus the fact that it narrows this inside the class too.

for a senior

Justify the modelling choice: instance-owned state and discoverability argue for this is T; externally supplied plain data and composability argue for a free guard. Name the coupling cost of the subclass form and the risk of mutation between check and use.

for a principal

Decide the house style for expressing object states — predicate methods, discriminated data, or parsed types at the boundary — because mixing all three across a codebase leaves invariants encoded in three incompatible ways and no single place to review them.

## The form A type predicate normally names a parameter: `function isString(v: unknown): v is string`. A method may instead name the receiver: ```ts class Draft { constructor(public title: string, public body?: string) {} isPublishable(): this is Draft & { body: string } { return this.body !== undefined; } } ``` The return type `this is T` says: this method returns a boolean, and a `true` result licenses the compiler to treat *the object the method was called on* as `T`. ## What it does at the call site ```ts const draft = new Draft("Title", "Body"); if (draft.isPublishable()) { draft.body.trim(); // body: string, not string | undefined } ``` The narrowing lands on the receiver expression, following the same rules as any other narrowing: it works on a reference the checker can track and it resets if the reference is reassigned. It also works on `this` itself: ```ts class Draft { // ... publish(): string { if (this.isPublishable()) { return this.body.toUpperCase(); // this narrowed inside the method } throw new Error("Draft has no body"); } } ``` Interfaces can declare the same signature, so a contract can require implementors to provide the narrowing check without dictating how it is computed. ## Two shapes it takes **Refinement by intersection.** The asserted type is the class itself intersected with a tighter view of some members — `this is Draft & { body: string }`. This is the workhorse form for objects with optional or nullable state: one method encodes "I am in the filled state" and every caller gets the tightened field. **Refinement to a subclass.** In a hierarchy, the base declares checks that assert subclass types: ```ts class FsNode { isDirectory(): this is Directory { return this instanceof Directory; } } class Directory extends FsNode { children: FsNode[] = []; } ``` Callers holding a `FsNode` can branch once and reach `children` without writing `instanceof` themselves or importing the subclass to do it. ## When to choose it over a free guard Ask who owns the state being tested. Choose `this is T` when the object owns it: the check is really a question about an instance's own lifecycle or variant, the instance is what callers already hold, and discoverability matters — autocomplete on the object surfaces `isPublishable()` where nobody would think to import a helper. It also keeps the invariant next to the fields it reasons about, so a change to the state representation updates the check in the same file. Choose a standalone `value is T` guard when the value comes from outside as plain data — a parsed payload, a message, a value typed `unknown` — because there is no instance whose class you control, and because a free function composes: it can be passed to `filter`, reused across shapes, and applied to values you did not construct. ## Costs worth naming in an interview The subclass form points the base class at its own subtypes, which inverts the usual dependency direction and can force a circular import; the intersection form avoids that and is usually the better default. The narrowing also depends on calling the method *on the reference*: pulling the method off the object (`const check = draft.isPublishable; if (check()) …`) loses both the narrowing and the `this` binding. And because objects are mutable, a narrowed receiver is only as good as the code between the check and the use — an intervening call that clears the field is not tracked. Finally, `this is T` is a different feature from a method that *returns* `this`. The latter is about fluent chaining and preserves the subclass type through a chain of calls; `this is T` returns a boolean and refines the receiver in a branch. Interviewers sometimes probe whether a candidate conflates the two.

  • Does the narrowing survive if you pull the method off the object first?
    No. `const check = draft.isPublishable; if (check()) { … }` narrows nothing, because the compiler only applies a `this is T` predicate to the receiver of the call — and at runtime the detached method has lost its `this` binding as well, so it is broken twice over.
  • Can an interface declare a `this is T` method?
    Yes. An interface method signature may declare `this is T`, so a contract can require implementors to expose the check while leaving the implementation open. Callers narrow through the interface without knowing which class they hold.
  • What is the drawback of the `isFile(): this is FileRep` subclass style?
    The base type has to name its subtypes, which inverts the dependency direction and often forces a circular import. When you only need tighter fields rather than a different class, an intersection such as `this is Draft & { body: string }` gives the same call-site benefit without the coupling.
  • How is this different from a method that returns `this`?
    Returning `this` is a polymorphic return type for chaining — each call hands back the same (possibly subclass) instance. `this is T` returns a boolean and only affects what the compiler believes about the receiver inside a branch. Different features, easily confused by name.

saying these in an interview costs you the question

  • Confuses `this is T` with returning `this` for chaining
  • Thinks the method returns the narrowed object rather than a boolean
  • Assumes the check re-runs whenever the object changes
  • Believes `this is T` performs a runtime cast of the instance
  • Says only classes can declare it, not interfaces

context