Returner underklassen i den abstrakte klasse i TypeScript


Jeg har forenklet mit problem til følgende situation:

abstract class Foo {
  child: Foo
  foo(): Foo {
    return this.child
  }
}
class Bar extends Foo {
  bar = true
}
const foo = new Bar()
// Property 'bar' does not exist on type 'Foo'. ts(2339)
foo.foo().bar

Denne fejl er selvforklarende og ikke overraskende, men det jeg ønsker at opnå er, at foo() returnerer noget af underklasse-typen. I dette tilfælde ville det være Bar, sådan at jeg kan bruge dens egenskab bar. Vi kunne opnå dette på følgende måde:

abstract class Foo<C extends Foo<C>> {
  child: C
  foo(): C {
    return this.child
  }
}
class Bar extends Foo<Bar> {
  bar = true
}
const foo = new Bar()
foo.foo().bar

Jub, fejlen er løst! Men jeg vil også gerne bruge this inde i Foo på samme måde som jeg bruger C, altså subklasse-typen:

abstract class Foo<C extends Foo<C>> {
  foo(): C {
    // Type 'this' is not assignable to type 'C'.
    // 'this' is assignable to the constraint of type 'C',
    // but 'C' could be instantiated with a different subtype of constraint 'Foo<C>'. ts(2322)
    return this
  }
}
class Bar extends Foo<Bar> {
  bar = true
}
const foo = new Bar()
foo.foo().bar

Vi kan dog ikke, da TypeScript ikke er klar over, at den underklasse-type, der er givet som en typeparameter, faktisk er den samme som den faktiske underklasse-type.

Er der en måde at gribe dette an på, så jeg ikke får denne fejl?

Jeg kunne omgå det ved at caste this sådan her: this as unknown as C, hvilket fortæller TypeScript, at vi stoler på, at det faktisk er det samme, men da jeg har masser af kode, der bruger this i en kontekst hvor C forventes, er det noget, jeg helst ikke vil gøre.

Rediger: I stedet for at have en baseklasse, startede jeg med at bruge nogle få funktioner, der indeholdt den delte funktionalitet:

interface Foo {
  child: Foo
}
function fooFn<C extends Foo>(parent: C, child: C): C {
  return child
}
class Bar implements Foo {
  child: Bar
  bar = true
  foo() {
    return fooFn(this, this.child)
  }
}
const foo = new Bar()
foo.foo().bar

Dette fungerede godt for mit brugstilfælde, men nu hvor det blev til omkring 20 sådanne funktioner, følte jeg, at standardkoden ved at have disse metoder direkte kalde funktionerne kunne erstattes af en baseklasse, men så stødte jeg på dette problem.

Svar #1

Jeg kiggede mere på dette mønster, da jeg så nogle vage omtaler af, at det også blev brugt i C++ og C#, og åbenbart kaldes det Curiously Recurring Template Pattern (CRTP). Og de laver bare en typekonvertering af this i deres eksempler, så det virker som den legitime måde at gøre det på. Da jeg er mere fortrolig med C#, ledte jeg efter et eksempel på det i stedet og fandt et opslag med dette eksempel:

public abstract class AlgorithmBase<TDerived>
    where TDerived : AlgorithmBase<TDerived>
{
    public TDerived Run()
    {
        this.Step1();
        this.Step2();
        // ...
        this.StepN();
        this.FinalStep();
        
        return (TDerived)this;
    }
    
    protected abstract void Step1();
    protected abstract void Step2();
    // ...
    protected abstract void StepN();
    protected abstract void FinalStep();
}

Hvilket oversat til TypeScript ville blive:

abstract class AlgorithmBase<TDerived extends AlgorithmBase<TDerived>> {
  run(): TDerived {
    this.step1();
    this.step2();
    // ...
    this.stepN();
    this.finalStep();
    
    return this as any; // or: as unknown as TDerived,
    // but that is unnecessary as we already gave an explicit return type
  }

  protected abstract step1();
  protected abstract step2();
  // ...
  protected abstract stepN();
  protected abstract finalStep();
}

Hvis der fandtes bedre måder at gøre dette på, ville jeg have forventet, at de havde wikipedia-sider og indlæg om dem i stedet, så jeg tror, at dette er så godt, som det bliver.

Svar #2

Du kæmper imod typescripts typekontrol.

Der er altid en klodset måde at omgå det på. Typen any kan bruges til at returnere C:

return this as any

Typescript gør, hvad der forventes, og beskytter dig mod dig selv. At tvinge et objekt ind i en udvidet klasse fører til datatab, så du skal vise compileren, at du ved dette.

Relaterede artikler