When it comes to polymorphic behavior in TypeScript, the use of this
is not as straightforward as it may seem. In reality, this
serves as a type parameter for your class, albeit a hidden one managed by the compiler. Since this
is essentially a type parameter with a constraint that extends the current class (similar to
class Base<This extends Base> {}
), its full definition remains unknown within the class.
Various questions arise regarding why assigning a value to a variable with a generic type parameter or working with conditional/mapped types alongside type parameters can be challenging. An example that highlights this issue is:
function partial<T extends { prop: number }>(): Partial<T> {
return { prop: 1 }
}
The underlying reason behind these challenges lies in the fact that we only have knowledge of the minimal interface that T
must adhere to, rather than the complete structure of T
. Consider the following scenario:
function getObj<T extends { prop: { name: string } }>(): Partial<T> {
return { prop: { name: "" } }// still an error
}
var r = getObj<{ prop: { name: string, lastName: string}}>();
r.prop!.lastName // r should have last name, where is my lastName ?!
In this case, while the constraint on prop
in
T</code mandates the presence of <code>name
, additional properties like
lastName</code are allowed, leading to potential discrepancies when dealing with concrete values and type parameters.</p>
<p>This same reasoning applies to polymorphic <code>this
, where due to its incomplete nature as a type parameter, direct assignment of object literals becomes problematic since the final shape of
this
remains ambiguous.
Consider a parallel example:
class Base {
update(modifier: Partial<this>) {
Object.keys(modifier).forEach(key => {
this[key] = modifier[key];
});
}
}
class Sub extends Base {
prop! :{
name: string
};
job() {
this.update({ prop: { name: string } }); // not valid for any class, example below
}
}
class Sub2 extends Sub {
prop! : {
name: string
lastName: string
};
}
To address such issues, using a type assertion provides a workaround at the cost of compromising type safety, or incorporating a second type parameter representing the class properties restricts extending classes to a single level for adding new properties.
class Base<TProps> {
update(modifier: Partial<TProps>) {
Object.assign(this, modifier);
}
}
class Sub extends Base<Sub> {
prop: number;
job() {
this.update({ prop: 1 });
}
}