microsoft / microsoft/TypeScript

instanceof narrowing should preserve generic types from super to child type

Ouverte
#28,560 12 commentaires 10 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Domain: check: Control Flow In Discussion Suggestion
Langage dominant
Go
Étoiles
111k
Forks
14.4k
Merge moyen
1 j 19 h
PR mergées (30 j)
117

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

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Aucun fichier ni test n'est nommé. Commencez par réduire les exemples de Parent/Child, Fetcher et Dependency, et par suivre le narrowing de instanceof de TypeScript parallèlement à l'inférence des types génériques. C'est terminé lorsque les arguments génériques réduits sont préservés là où ils peuvent être inférés, et que les cas d'union documentés se comportent de manière cohérente.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
typescript
Domaine
compilers
Type d'issue
Fonctionnalité
Difficulté
5/5
Temps estimé
Plus d'une semaine
Activité
À l'abandon
Clarté
Plutôt claire
Accessibilité débutants
30/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.