skip to content

In a TypeScript class, what does declaring a method as `isDirectory(): this is Directory` give you that declaring it `isDirectory(): boolean` does not?

level: middleimportance: should knowfreq 32%

answer

  1. a boolean the compiler can act on
  2. the predicate's subject is the receiver
  3. narrows the object in the true branch
  4. trusted, never verified by the compiler
  5. erased — only the body's check runs

basics

~20 s

A this-based type predicate narrows the receiver: inside a branch where the call returned true, the compiler treats the object the method was called on as a Directory. A plain boolean return narrows nothing at all.

solid answer

~50 s

Both methods return a boolean at runtime; only one of them talks to the checker. `isDirectory(): this is Directory` declares a type predicate whose subject is the receiver, so in the true branch of `if (fso.isDirectory())` the compiler narrows `fso` itself to `Directory` and lets you reach members that only `Directory` has. With `: boolean`, the object keeps its declared type and you would need an `instanceof` check or an assertion at the call site instead. The predicate can also narrow to an intersection, as in `this is Networked & this`. Two caveats worth saying out loud: the compiler checks only that the body returns a boolean, not that the boolean is *true* for the right objects — the claim is trusted — and the whole predicate is erased, so the only runtime check is whatever the body actually does.

code

typescript · 29 lines
typescript
class FileSystemObject {
  constructor(public path: string) {}

  isFile(): this is FileRep {
    return this instanceof FileRep;
  }

  isDirectory(): this is Directory {
    return this instanceof Directory;
  }
}

class FileRep extends FileSystemObject {
  constructor(path: string, public content: string) {
    super(path);
  }
}

class Directory extends FileSystemObject {
  children: FileSystemObject[] = [];
}

function describe(fso: FileSystemObject): string {
  if (fso.isFile()) return `file of ${fso.content.length} chars`;
  if (fso.isDirectory()) return `dir of ${fso.children.length} entries`;
  return fso.path;
}

console.log(describe(new FileRep('/a.txt', 'hi')));

go deeper

for a junior

Know that a method can declare a return type of the form this is SomeType, and that its effect is to let an if-statement treat the object as that type inside the branch.

for a middle

Explain that the predicate's subject is the receiver rather than a parameter, that the asserted type must be compatible with the declaring class, and that an intersection form adds capabilities without discarding what is known.

for a senior

Demonstrate the trust model: the compiler checks only that the body returns a boolean, so a wrong guard produces code that compiles and then breaks. Say where the real check belongs and how you keep it honest.

for a principal

Frame it as an encapsulation decision: a guard method keeps knowledge of concrete subtypes inside the hierarchy, so the representation can change without touching callers — at the cost of an unverified assertion in your public surface.

## Two signatures, one runtime behaviour ```ts class FileSystemObject { constructor(public path: string) {} isDirectory(): boolean { return this instanceof Directory; } } ``` Call that in an `if` and the compiler learns nothing: `fso` is still a `FileSystemObject` in the true branch, and touching a `Directory`-only member is an error. The boolean told the program something the type system was not invited to hear. ## What `this is T` declares Writing the return type as `this is Directory` turns the method into a **type predicate whose subject is the receiver**. The signature says: "when this method returns true, the object it was called on is a `Directory`." The compiler takes that at face value and narrows the receiver expression in the true branch. ```ts class FileSystemObject { constructor(public path: string) {} isFile(): this is FileRep { return this instanceof FileRep; } isDirectory(): this is Directory { return this instanceof Directory; } } class FileRep extends FileSystemObject { constructor(path: string, public content: string) { super(path); } } class Directory extends FileSystemObject { children: FileSystemObject[] = []; } declare const fso: FileSystemObject; if (fso.isFile()) { fso.content; // fso: FileRep } else if (fso.isDirectory()) { fso.children.length; // fso: Directory } ``` The asserted type has to be compatible with the class the method is declared on — you cannot claim the receiver is something unrelated. An intersection form is allowed too, which is how you model a capability rather than a subclass: `isNetworked(): this is Networked & this` keeps everything already known about the object and adds the extra members. ## The declaration is a promise, not a proof The compiler verifies that the body returns a boolean. It does **not** verify that the boolean is true exactly when the claim holds. This is the same trust model every type predicate runs on, and it is the standard unsoundness to flag in an interview: ```ts isDirectory(): this is Directory { return true; // compiles; every caller now believes it } ``` Code narrowed by that guard compiles happily and then reads `children` off an object that has none. So the body should contain a check that genuinely establishes the claim — an `instanceof`, a discriminant field read, a structural test — and the guard should live next to it so the two never drift apart. ## Why put the check on the class at all The alternative is for every caller to write `instanceof` themselves, which spreads knowledge of the concrete subclasses across the codebase. A `this is T` method keeps that knowledge inside the hierarchy: callers ask a question in the base class's vocabulary (`isDirectory()`), and the base class decides how the question is answered. Change the representation later — swap `instanceof` for a flag — and the callers do not move. It is also available on interfaces, so a contract can advertise the narrowing without naming an implementation: ```ts interface Openable { isOpen(): this is { close(): void }; } ``` ## What it does not do The predicate narrows the receiver *expression*, in the branch where the call was made, for as long as the compiler can keep tracking that expression. It is not a runtime facility: nothing in the emitted JavaScript records the narrowed type, and no check is inserted on your behalf. The compiled method is exactly its body — the `instanceof` you wrote and nothing else. And it narrows nothing about other variables that happen to hold the same object; only the expression the method was called on is affected. ## The short version for an interviewer `: boolean` answers a question. `: this is Directory` answers the question *and* tells the checker what the answer implies about the receiver. The second costs nothing at runtime, is trusted rather than verified, and is only as honest as the body you wrote underneath it.

  • What happens if the body of a `this is Directory` method returns true for an object that is not a Directory?
    It compiles. The checker only requires the body to return a boolean; the claim itself is trusted. Callers then read `Directory`-only members off the wrong object and fail at runtime. The guard is exactly as trustworthy as the check inside it, which is why the check belongs in the body and nowhere else.
  • Can this form appear on an interface member rather than a class method?
    Yes. An interface can declare `isOpen(): this is { close(): void }`, and every implementer instantiates the receiver type with its own. That lets a contract advertise the narrowing without naming a concrete class, and keeps callers from reaching for instanceof against implementations they should not know about.
  • Does the predicate emit any runtime code?
    None. The return type is erased like every other annotation, so the compiled method is precisely the body you wrote — typically an instanceof or a field test. Nothing checks the claim at run time, and there is no metadata anywhere recording that the method was a guard.

saying these in an interview costs you the question

  • Thinks the compiler verifies the body proves the claim
  • Believes the predicate inserts a runtime type check
  • Says a boolean return narrows the receiver anyway
  • Confuses it with a predicate on a parameter
  • Claims it works only for classes, never interfaces

context