Skip to content

instanceof narrowing should preserve generic types from super to child type #28560

Open
@Nathan-Fenner

Description

@Nathan-Fenner

Search Terms

  • instanceof generic
  • narrow instanceof

Suggestion

The following code has a rather unexpected behavior today:

class Parent<T> {
    x: T;
}
class Child<S> extends Parent<S> {
    y: S;
}

function example(obj: Parent<number>) {
    if (obj instanceof Child) {
        const child = obj;
    }
}

The narrowed type that child gets is Child<any>.

I'm requesting that the type instead gets narrowed to Child<number>, which is what I originally assumed would happen.

My proposal is, in general, to infer the type arguments to the narrowed-to class type whenever they can be determined from the original type.

Precise Behavior

Consider the following code today:

const x: P = ...;
if (x instanceof C) {
    x;
}

Today, if C is a generic class, x gets the narrowed type C<any, any, any, ...>; otherwise it just gets the type C.

My proposal is to consider the following piece of code, and use it to inform

Imagine that C has a no-argument constructor. Then the following piece of code is valid today:

function onlyP(c: P) { ... }

onlyC(new C());

in order to make it valid, the compiler infers type arguments for C that make it into a subtype of P. My proposal is to use the same strategy to infer the type arguments for C in an instanceof narrowing.

Consider the following examples today, and their corresponding instanceof narrowings:

function ex1(x: Parent<string>) { }
ex1(new Child()); // inferred arguments: <string>

function ex2(x: Parent<number | string>) { }
ex2(new Child()); // inferred arguments: <number | string>

function ex3(x: Parent<number> | string) { }
ex3(new Child()); // inferred arguments: <number>

function ex4(x: Parent<number> | Parent<string>) { }
ex4(new Child()); // inferred arguments: Child<number | string>
// Note: the above errors, because it Child<number|string> actually fails to be a subtype of
// Parent<number> | Parent<string>.
// We can either choose to infer Child<any> in this case, or use the (incorrect, but more-precise)
// inference Child<number | string>.

Examples

The original use-case I had in mind was roughly the following:

abstract class Obtainer<T> {
  __phantom: T = null as T;
}

abstract class Fetcher<T> extends Obtainer<T> {
  public abstract fetch(): Promise<T>;
}

abstract class Dependency<T, D> extends Obtainer<T> {
  public abstract dependencies(): Obtainer<D>;
  public abstract compute(got: D): T;
}

async function obtain<T>(obtainer: Obtainer<T>): Promise<T> {
  if (obtainer instanceof Fetcher) {
    // obtainer: Fetcher<T>
    // currently, it's a Fetcher<any>
    return await obtainer.fetch();
  }
  if (obtainer instanceof Dependency) {
    // obtainer: Dependency<T, any>
    // currently, it's a Dependency<any, any>
    const dependencies = obtainer.dependencies();
    return obtainer.compute(await obtain(dependencies));
  }
  throw new Error("not implemented");
}

(note: there's still one extraneous any in the above, since the D parameter cannot be inferred)

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
    • This is a breaking change. However, I think it could only really break code that was already broken. If really necessary, it could be put behind a new --strictGenericNarrowing flag.
  • This wouldn't change the runtime behavior of existing JavaScript code
    • No change to runtime; just inferred generic types.
  • 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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
    • I think this is an easy win for a more sound type system without any negative impact

Also, I am interested in contributing this change if it's approved!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Domain: Control FlowThe issue relates to control flow analysisIn DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions