microsoft / microsoft/TypeScript
instanceof narrowing should preserve generic types from super to child type
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 111k
- Forks
- 14.3k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 132
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
--strictGenericNarrowingflag.
- 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!
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
No files or tests are named. Start by reducing the Parent/Child, Fetcher, and Dependency examples and tracing TypeScript's instanceof narrowing alongside generic type inference. Done means narrowed generic arguments are preserved where they can be inferred, with the documented union cases behaving consistently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100