question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

instanceof narrowing should preserve generic types from super to child type

See original GitHub issue

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!

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:3
  • Comments:12 (7 by maintainers)

github_iconTop GitHub Comments

1reaction
Nathan-Fennercommented, Sep 14, 2020

@thw0rted I agree that it’s very counterintuitive, but it turns out that the types do overlap in this case. The reason is that there appears to be a chain of legal subtype casts that make this happen.

Here’s an example of an instance of Child, stripping away the prototype, constructor, methods, etc., which could affect variance but we’ll pretend everything is largely covariant here: { t: 'hi', a: 'bye', b: 5 }. This clearly “is” both a Child<string | number> and also a Parent<string, number> and also a Parent<string | number, string | number>, because it has all of the properties that each of those types require, which is all we really mean when we say “x has type T”.

It also turns out in this case that the most general sound type we can assign says exactly that: it is both a Child<string|number> and also a Parent<string, number>, so it is a Child<string|number> & Parent<string, number>. The type annotations tell us what we’re allowed to do with an object; so there’s not always some “best” type (e.g. is a {x: 4, y: "hi"} an {x:number,y:string} or is it a {x:4,y:"hi"} or is a {x:string|number,y:string|number}? The answer is: it’s essentially all of them at once. The annotation tells you what you can do with it and what you can expect from it, but not what it is.


The benefits of a fully general existential type approach is that you’d be able to largely side-step these complexities. You’d immediately be able to narrow it down to something like Parent<string, number> & exists<T where Child<T> extends Parent<string, number>> Child<T> which is crude and complicated looking, but definitely correct.

Then you’d have the task of trying to simplify away that scary-looking type, which hopefully could be built on some nice theory instead of ad-hoc patches to inference.

0reactions
thw0rtedcommented, Sep 15, 2020

Thanks Nathan, I hadn’t considered the case of a union type for T but in hindsight it’s obvious.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Typescript type narrowed to never with instanceof in an if- ...
Presumably you want each class to know that its model is the narrowed type, right? So doing the above narrowing of model both...
Read more >
Documentation - Narrowing
Argument of type 'string | number' is not assignable to parameter of type ... In many editors we can observe these types as...
Read more >
Generics - mypy 0.991 documentation
The built-in collection classes are generic classes. Generic types have one or more type parameters, which can be arbitrary types. For example, dict[int,...
Read more >
typescript-cheatsheet - GitHub Pages
If no types are declared, TypeScript will automatically assign a type depending ... Note that super has to be called in the constructor...
Read more >
2.3 Inheritance in SQL Object Types
With object types in a type hierarchy, you can model an entity such as a ... A type can have multiple child subtypes,...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found