I TypeScript, hur härleder egentligen typen 'this' sin typ?


I TypeScript skulle jag vilja använda this-nyckelordet för att typa vissa egenskaper i min klass. Dock stöter jag på ett problem som jag inte kan lösa. Det jag skulle vilja göra är något som:

export class Animal{
/*some properties*/
}

export class Population{
    AnimalConstructor : typeof Animal = Animal;
    animal : InstanceType<this['AnimalConstructor']>;

    createAnimal(){
        this.animal = new this.AnimalConstructor();//Type 'Animal' is not assignable to type 'InstanceType<this["AnimalConstructor"]>'
    }
}

Genom att göra det får jag felet: Type 'Animal' is not assignable to type 'InstanceType<this["AnimalConstructor"]>' på rad 10. Dock fungerar denna kod bra:

export class Animal{
/*some properties*/
}

export class Population{
    AnimalConstructor : typeof Animal = Animal;
    animal : Animal;

    createAnimal(){
        this.animal = new this.AnimalConstructor();
    }

}

Jag kan inte förstå varför den sista fungerar och den första inte gör det. Detta beror förmodligen på sättet som kompilatorn härleder typen av this-nyckelordet. Men jag hittade inga dokument som kan förklara det beteendet heller. Den officiella dokumentationen säger bara: 'en speciell typ kallad this hänvisar dynamiskt till typen av den aktuella klassen.' - vilket inte förklarar varför exemplet ovan inte fungerar.

Svar #1

Den polymorfa this-typen fungerar som en implicit generisk typparameter som är begränsad till den aktuella klassen, men specificeras endast som en typargument när du får tillgång till en specifik instans av klassen eller dess subklass. (Se microsoft/TypeScript#4910 för pull requesten som implementerar och beskriver this som en implicit typparameter.) Det betyder att när du använder this-typen får du fördelarna - och nackdelarna - med generiska typer.

Utilitytypen InstanceType<T> är implementerad som en konditionell typ, vilket du kan se från dess definition:

type InstanceType<T extends abstract new (...args: any) => any> = 
  T extends abstract new (...args: any) => infer R ? R : any;

Så, inuti kroppen för Population-klassdefinitionen är typen InstanceType<this['AnimalConstructor']> en generisk villkorlig typ (en villkorlig typ som beror på minst en ännu icke specificerad typparameter). Och, tyvärr, kan kompilatorn verkligen inte resonera om sådana typer.


När man utvärderar värdet new this.AnimalConstructor() vidgar kompilatorn den uppenbara typen av this till Animal, eftersom du får tillgång till en specifik egenskap på ett generiskt typat värde, och kompilatorn gör denna vidgning som en approximation för att göra saker enklare. (Se denna kommentar på microsoft/TypeScript#33181 för en källa.) Och således ses this.AnimalConstructor som typen typeof Animal och därför ses new this.AnimalConstructor() som typen Animal:

const ac = this.AnimalConstructor;
//const ac: typeof Animal
const a = new ac();
//const a: Animal;

Men kompilatorn skjuter upp utvärderingen av generiska villkorstyper som InstanceType<this["AnimalConstructor"]>, och behandlar därför sådana typer som i princip opak. Om du försöker tilldela ett värde till en variabel av en sådan typ kommer kompilatorn nästan säkert att klaga eftersom den inte kan verifiera att värdet är kompatibelt med typen. Som människa kan du undersöka värdet och förstå betydelsen bakom villkorstypen och säga "ja, detta är okej", men kompilatorn ser mest typen som en svart låda och har ingen aning om vad som kan vara kompatibelt med den. (Det närmaste man kan komma dokumentation för detta problem är microsoft/TypeScript#33912).

Så får du ett fel:

this.animal = a; // error!
// Type 'Animal' is not assignable to 
// type 'InstanceType<this["AnimalConstructor"]>' 😟

Om du vill behålla dina typer som de är, är förmodligen det bästa sättet framåt att helt enkelt acceptera att du är smartare än kompilatorn. Eftersom du är säker på att new this.AnimalConstructor() tydligt är av typen InstanceType<this["AnimalConstructor"]> oavsett vad this visar sig vara i subklasser, kan du helt enkelt hävda den fakta för kompilatorn för att förhindra att den oroar sig över saker den inte kan räkna ut:

createAnimal() {
  const ac = this.AnimalConstructor;    
  const a = new ac();
  this.animal = a as InstanceType<this['AnimalConstructor']>; // okay
}

eller bara

createAnimal() {
  this.animal = new this.AnimalConstructor() as typeof this.animal; // okay
}

Det fungerar och du kan gå vidare. Det är inte lika typ-säkert som det skulle kunna vara om kompilatorn vore smartare, men det är förmodligen det bästa du kan göra utan omstrukturering... till exempel, för att göra Population uttryckligen generisk i instanstypen av AnimalConstructor så att du kan kontrollera när generics utvidgas och undvika villkorliga typer helt och hållet:

export class Population<T extends Animal> {
  constructor(public AnimalConstructor: new () => T) {
    this.animal = new AnimalConstructor(); // things should be initialized
  }
  animal: T

  createAnimal() {
    this.animal = new this.AnimalConstructor(); // okay
  }
}

Lekplatslänk till kod

Relaterade artiklar