I have simplified my problem to the following 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
This error is self explanatory and unsurprising, but what I want to achieve is that foo()
returns something of the subclass type. In this case that would be Bar
, such that I can use its property bar
. We could achieve this like so:
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
Yay, the error is solved! However I also want to use this
inside Foo
the same way I use C
, i.e. the subclass type:
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
We can't though, as TypeScript isn't aware that the subclass type passed as a type parameter is actually the same as the actual subclass type.
Is there a way to approach this different such that I won't have this error?
I could workaround it by casting this
like so: this as unknown as C
, letting TypeScript know to trust us that it actually is the same, but as I have plenty of code using this
in a context where C
is expected, this is something I rather not do.
Edit: Rather than having a base class I started out using a few functions that contained the shared functionality:
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
This worked well for my use case, but now that it became like 20 such functions, I felt the boilerplate of having those methods directly calling the functions could be replaced by a base class, but then I ran into this issue.
CodePudding user response:
You are fighting against the type checking of typescript.
There is always the hacky way around it. The any type can be used to return C:
return this as any
Typescript is doing what is expected, and protects you from yourself. Forcing an object into an extended class leads to data loss, so you need to show the compiler you know this.
CodePudding user response:
I looked more into this pattern as I saw some vague mentions of it being used too in C and C# and apparently it is called the Curiously Recurring Template Pattern (CRTP). And they just cast this
too in their examples, so seems like the legit way to go about it. As I am more familiar with C#, I looked for an example of it instead and found a post with this example:
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();
}
Which translated to TypeScript would become:
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();
}
If there were better ways of going about this, I would have expected them to have wikipedia pages and posts about them instead, so I think this is as good as it gets.