Narrowing down a type without having to declare an ad-hoc type guard function?

Say, I have a class like this, and TypeScript is not happy about it:

class Foo extends ThirdPartyLibrary {
  animal!: Animal;

  bar(): void {
    this.animal.quack(); // Method `quack` does not exist on type `Animal` 😱
  }
}

The official way to resolve this seems to be using an ad-hoc type narrowing function:

class Foo extends ThirdPartyLibrary {
  animal!: Animal;

  bar(): void {
    function isDuck(maybeDuck: unknown): maybeDuck is Duck {
      return maybeDuck?.look === 'ducky' && maybeDuck?.voice === 'quacky';
    }

    if (!isDuck(this.animal)) {
      throw new Error('Expected argument to be a duck');
    }

    this.animal.quack();
  }
}

In certain cases, such runtime checks are an overkill. For example, when using a poorly typed third-party library, I don't want to test the library in runtime and write all that boilerplate code.

Instead, I want to write it as short as possible.

My best attempt so far is to reassign the variable to another variable with as type casting:

class Foo extends ThirdPartyLibrary {
  animal!: Animal;

  bar(): void {
    const animal = this.animal as unknown as Duck;

    animal.quack(); // Now TypeScript knows that this animal can quack
  }
}

But I don't need that variable! I only need TypeScript to know the type statically and I don't want to declare any runtime variables or if-clauses or errors thrown.

I'm imaginging something like this:

class Foo extends ThirdPartyLibrary {
  animal!: Animal;

  bar(): void {
    this.animal is Duck; // Narrow down the type without boilerplate

    this.animal.quack();
  }
}

I just want TypeScript to statially know that at this point in code this variable/property is certain to be of a specific type. How do I do that?


Solutions that do not work for me:

  1. Type the animal property to be a Duck:

    class Foo extends ThirdPartyLibrary {
      animal!: Duck;
    }
    

    This solution is the most rational for my simple example, but that's just a minimal artifical example I came up with.

    Imagine that the Foo class is more complicated than that: this.anmial is used many times, and most of the time it can be any Animal. Only at this specific point in code it's known for certain to be a duck.

  2. Use inline type casting: (this.animal as unknown as Duck).quack().

    This works, but when you need to do more than one ducky thing with this animal, this approach becomes annoying:

    (this.animal as unknown as Duck).quack();
    (this.animal as unknown as Duck).waddle();
    (this.animal as unknown as Duck).awwYiss("Motha. Funkin. Bread crumbs!");
    
  3. Fixing the typings of the third-party library. Assume that the typings are really compliated and you don't have the capacity to dig into them.

Upvotes: 0

Views: 124

Answers (1)

daylily
daylily

Reputation: 916

You can define a phantom narrowing predicate function that doesn't have as much runtime cost:

function narrow<U>(x: any) : x is U { return true }

...

bar(): void {
  if (narrow<Duck>(this.animal))
    this.animal.quack();
}

But I don't think this is close enough to your best expectation.

Upvotes: 1

Related Questions