Description
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 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
- 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!