Överbelastning av klassens egenskaper baserat på konstruktörargument i TypeScript
I vår kodbas använder vi navigerings- och builder-mönster ganska omfattande för att abstrahera bort sammansättningen av hierarkiska objekt. I hjärtat av detta finns en Navigator-klass som vi använder för att traversera olika klasser. Jag försöker för närvarande migrera detta till TypeScript men har svårt att typa det för att utnyttja TypeScripts kraft.
Jag tror att kärnan i mitt problem är att jag inte kan använda this som standardvärde för en generisk typ i en klass, t.ex. class Something<T=this>, eller att jag inte kan överbelasta klassen för att på något sätt villkorsvis sätta typerna på klassens egenskaper. Kan du ge några insikter om hur jag skulle kunna typa Navigator (och byggarklasserna) nedan?
// I guess what I'd like to do is
// class Navigator<BackT = this> but that's not allowed
class Navigator<BackT> {
// It's this 'back' type I'll like to define more specifically
// i.e. if back is passed to the constructor then it should be 'BackT'
// if back is not passed to the constructor or is undefined, it
// should be 'this'
back: BackT | this;
constructor(back?: BackT) {
this.back = back || this;
}
}
class Builder1<BackT> extends Navigator<BackT> {
builder1DoSomething() {
// Do some work here
return this;
}
}
class Builder2<BackT> extends Navigator<BackT> {
withBuilder1() {
return new Builder1(this);
// Also tried the following, but I got the same error:
// return new Builder1<this>(this);
}
builder2DoSomething() {
// Do some work here
return this;
}
}
// This is fine
new Builder1().builder1DoSomething().builder1DoSomething();
new Builder2()
.withBuilder1()
.builder1DoSomething()
// I get an error here becuase my types are not specific enough to
// let the complier know 'back' has taken me out of 'Builder1' and
// back to 'Builder2'
.back.builder2DoSomething();
lekplatslänk
Svar #1
Du kan använda en villkorlig typ på fältet back för att typa det som this om inget typargument gavs till klassen. Vi kommer att använda typen void som standard för att signalera frånvaron av ett typargument:
class MyNavigator<BackT = void> {
back: BackT extends void ? this : BackT; // Conditional type
constructor(back?: BackT) {
this.back = (back || this) as any;
}
}
class Builder1<BackT = void> extends MyNavigator<BackT> {
builder1DoSomething() {
return this;
}
}
class Builder2<BackT = void> extends MyNavigator<BackT> {
withBuilder1() {
return new Builder1(this);
}
builder2DoSomething() {
return this;
}
}
new Builder2()
.withBuilder1()
.builder1DoSomething()
// ok now
.back.builder2DoSomething();
Svar #2
Jag tror att ditt bästa alternativ är att skapa en speciell variant av varje klass som fungerar som ditt alternativ utan konstruktörsparameter. Så ha en speciell SelfNavigator, SelfBuilder1 osv. som utökas från sina motsvarande klasser och som själva inte har några generiska typer.
class MyNavigator<BackT> {
back: BackT;
constructor(back: BackT) {
this.back = back;
}
}
class SelfMyNavigator extends MyNavigator<SelfMyNavigator> {}
class Builder1<BackT> extends MyNavigator<BackT> {
builder1DoSomething() {
// Do some work here
return this;
}
}
class SelfBuilder1 extends Builder1<SelfBuilder1> {
constructor() {
super((null as unknown) as SelfBuilder1);
this.back = this;
}
}
class Builder2<BackT> extends MyNavigator<BackT> {
withBuilder1() {
return new Builder1(this);
}
builder2DoSomething() {
// Do some work here
return this;
}
}
class SelfBuilder2 extends Builder2<SelfBuilder2> {
constructor() {
super((null as unknown) as SelfBuilder2);
this.back = this;
}
}
// This is fine
new SelfBuilder1().builder1DoSomething().builder1DoSomething();
new SelfBuilder2()
.withBuilder1()
.builder1DoSomething()
.back.builder2DoSomething();
lekplatslänk
Observera att du inte kan kalla super(this) så de fula typomvandlingarna används. Det finns olika sätt att undvika detta för att göra koden snyggare, t.ex. konvertera allt till ett system av gränssnitt och abstrakta klasser så att fältet back inte finns i superklassen, utan snarare i ett supergränssnitt. Eller göra det till en getter och returnera this om det inte är satt, även om detta fortfarande kommer behöva några typomvandlingar.
