Repository: TypeScript
Issue: instanceof narrowing should use InstanceType
Labels: Suggestion,Awaiting More Feedback

# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## 🔍 Search Terms

<!--
  💡 Did you know? TypeScript has over 2,000 open suggestions!
  🔎 Please search thoroughly before logging new feature requests as most common ideas already have a proposal in progress.
  The "Common Feature Requests" section of the FAQ lists many popular requests: https://github.com/Microsoft/TypeScript/wiki/FAQ#common-feature-requests

  Replace the text below:
-->

instanceof, narrow, generic, typeof

## ✅ Viability Checklist

<!--
   Suggestions that don't meet all these criteria are very, very unlikely to be accepted.
   We always recommend reviewing the TypeScript design goals before investing time writing
   a proposal for ideas outside the scope of the project.
-->
My suggestion meets these guidelines:

* [ ] This wouldn't be a breaking change in existing TypeScript/JavaScript code. (Stronger narrowing logic can result in additional errors being emitted.)
* [x] This wouldn't change the runtime behavior of existing JavaScript code
* [x] This could be implemented without emitting different JS based on the types of the expressions
* [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
* [x] This feature would agree with the rest of [TypeScript's Design Goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals).


## ⭐ Suggestion

`value instanceof SomeClass` narrows `value` to SomeClass in subsequent flow. However, `value instanceof someClass`, where `someClass` is defined as `someClass: T` and `T extends typeof BaseClass`, only narrows `value` to `BaseClass` rather than `InstanceType<T>`.

See also #28936, though in this case I'm not referencing `constructor` at all.

## 📃 Motivating Example

<!--
  If you were announcing this feature in a blog post, what's a short explanation that shows
  a developer why this feature improves the language?
-->

If you want to downcast in a class hierarchy, you use `instanceof`. JavaScript's dynamism even allows you to dynamically pick the class to cast to. However, TypeScript doesn't understand that when the class is specified using generics, which prevents you from returning a corresponding instance type.

```typescript
class Base {
  private readonly field: number;
  constructor(field: number) {
    this.field = field;
  }

  private downcastTo<T extends typeof Base>(subclass: T): InstanceType<T> {
    if (this instanceof subclass)
      return this; // error: Type 'this' is not assignable to type 'InstanceType<T>'
    throw new TypeError(`expected ${subclass.name}, got ${this.constructor.name}`);
  }

  static parse<T extends typeof Base>(this: T, input: string): InstanceType<T> {
    let result: Base;
    if (input == "Sub1") {
      result = new Sub1(1);
    } else if (input == "Sub2") {
      result = new Sub2(2);
    } else {
      throw new TypeError("invalid input");
    }
    return result.downcastTo(this);
  }
}

class Sub1 extends Base {
  // ...
}
class Sub2 extends Base {
  // ...
}

// Normal static method usage
const base: Base = Base.parse("sub1")
// Now also valid, with stronger checks
const sub1: Sub1 = Sub1.parse("sub1");
```

## 💻 Use Cases

We are using this for a more realistic version of the example, parsing a serialized format into a type hierarchy. Some callers know that the serialized data should be a specific subclass, and that there should be an error if it doesn't match that subclass, and so it's convenient if the class you call `parse` on decides the type you get back.

Workarounds:
- Have `parse` always return Base, downcast at call sites. Verbose: `if (!(result instanceof Sub1)) throw new Error("…")`.
- Use a type-predicate wrapper function for `instanceof`. This works but is unchecked by the compiler, and of course adds a small bit of overhead because the function won't be compiled away.
- Use `as InstanceType<T>` to tell TypeScript we know what we're doing. Also works, also unchecked, but probably the workaround I'd recommend for now.