microsoft / microsoft/TypeScript

Make `instanceof` narrow better using an anonymous class returned from a function

Aperta
#57,317 4 commenti 3 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Domain: check: Control Flow Help Wanted Possible Improvement
Lingua principale
Go
Stelle
111k
Fork
14.4k
Merge medio
1g 19h
PR unite (30g)
117

Descrizione

🔎 Search Terms

Function return, anonymous class, instanceof, mixins, narrowing,

🕗 Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ for entries about instanceof.

I used every-ts (kudos to Jake Bailey for making it so easy) so I'm pretty confident this is true unless some random commit fixed it temporarily.

The closest entry is this: https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-x-instanceof-foo-narrow-x-to-foo

However as I understand it as long as if (x instanceof y) compiles, it should be narrowing x to basically typeof x & InstanceType<typeof y>. There might be some other edge cases I'm not aware of but as you'll see in the playground it seems like this isn't happening as precisely as desired with an anonymous class returned from a function.

⏯ Playground Link

https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABAWQIYGsCmBhANqgZwIB4AVAPgAoC4BbTAZShGGAC5FSBKRAbwFgAUIkQAnTM1FII+InyEiRNekxbBEAXkTLGzVgG4FiAL5DTgoRAQEoKVABMcsggDkQtAEaZRmu1jyEBJQAjABMAMxchoJWYDZ2jgFETKIwYADmvmj+zpQARMBwcHlRQpbWtmDuXqIAknFQqJCYvmCYAO4JToFunt6UpYJVfXUNTRCYAHQ6qgaIAPTziPXA3gSIhIgABsM1WxvrMLYEABZwILj2iF6TZTEV2nSY9TbjmBzg6GBw7UhaaFATpNRE17HQBohyIgAAyTACsiAA-Ig2p00IlnL0ahCOKiukkCCk0ukBtEYOpqE8Xo1mog0q9mnB1OjukQsd4eAJhI96NS3tMnrNgPoFksVmsDtsmgBPfaglFwWw7areLa3QTGIA

💻 Code
function MakeClass<T>(someStuff: T) {
  return class {
    someStuff = someStuff;
  }
}

const MadeClassNumber = MakeClass(123);
const MadeClassString = MakeClass("foo");

const numberInstance = new MadeClassNumber();
numberInstance.someStuff; // Infers as `number` as it should be.

const someInstance: unknown = Math.random() > 0.5 ? new MadeClassNumber() : new MadeClassString();
if (someInstance instanceof MadeClassNumber) {
  someInstance.someStuff; // Infers as `any` and not `number`.
}
🙁 Actual behavior

someInstance.someStuff is inferred as any.

🙂 Expected behavior

someInstance.someStuff is inferred as number.

Additional information about the issue

The code snippet is a simplified version of what I actually ran into. I was creating a bunch of classes for validation purposes. e.g. const Month = new NumberInRange(1, 12);, const Hour = new NumberInRange(0, 23);, const Minute = NumberInRange(0, 59);, or so on. You can imagine much more complicated validation of course.

Later on I wanted to be able to write x instanceof Hour so that I could firstly know that it'd already been validated and secondly know that it was specifically an Hour and not some other type. This might sound more convoluted but it meant validation could occur in one place and I could avoid the problem of all numbers seeming the same at runtime. For various reasons, in this case a discriminated union didn't end up being as ergonomic but these classes were put into a union and instanceof was basically trying to fulfill the same niche.

Now, you can always fix this from going from const MadeClass1 = MakeClass(123); to class MadeClass1 extends MakeClass(123) {}. So I've unblocked myself using that and I wouldn't classify the priority of fixing this very high. However I only happened to be informed enough that this seemed like a reasonable change to try to make. The errors I was getting had to do with the narrowing failing so I don't think the errors you get are intuitive and I think other developers might have given up this style.

In general it might bring extra benefits in terms of consistency in the TypeScript codebase to treat it this way. The issue seems to be that in normal contexts all instances of MadeClassNumber are "typed"* as MakeClass<number>.(Anonymous class) but the instanceof narrowing seems to lose track of that fact and will apply the base constraint of the parent class and then only narrow it based upon that. In this case that means it narrows to MakeClass<any>.(Anonymous class) which doesn't match InstanceType<typeof MadeClassNumber> like one would expect it to.

*I'm using the display of the type I happen to see but I understand there's no stable format nor way to refer to them by a valid identifier.

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia con il TypeScript Playground collegato e riproduci l'esempio MakeClass/instanceof. Il lavoro è completato quando il narrowing di someInstance fa inferire someInstance.someStuff come number invece di any; il payload non indica file o test del repository da esaminare.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
typescript
Ambito
compilers
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Ferma
Chiarezza
Abbastanza chiara
Idoneità per principianti
35/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.