The concept of the polymorphic this
type serves as an implicit generic type parameter that is restricted to the current class type, and is only specified when referring to a specific instance of the class or its subclass. This functionality is outlined in the implementing pull request microsoft/TypeScript#4910, which describes this
as an implicit type parameter. Using the this
type offers both advantages and disadvantages associated with generics.
The InstanceType<T>
utility type is structured as a conditional type, as demonstrated by its definition:
type InstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : any;
Within the Population
class definition, the type
InstanceType<this['AnimalConstructor']>
represents a
generic conditional type, where this conditional type relies on at least one unspecified type parameter. Unfortunately, the compiler struggles to reason about such types effectively.
When processing the expression new this.AnimalConstructor()
, the compiler widens the apparent type of this
to Animal
, due to accessing a specific property on a generic-typed value. This widening simplifies operations for the compiler. As a result, this.AnimalConstructor
is perceived as type typeof Animal
, hence new this.AnimalConstructor()
translates to type Animal
:
const ac = this.AnimalConstructor;
//const ac: typeof Animal
const a = new ac();
//const a: Animal;
However, the evaluation of generic conditional types like
InstanceType<this["AnimalConstructor"]>
is postponed by the compiler, treating such types nearly as
opaque. Consequently, assigning a value to a variable of this type often triggers errors from the compiler, unable to validate compatibility due to lack of understanding around these complex types. Human analysis may discern value meaning within the conditional type, but the compiler perceives it largely as an enigma. For reference, consult
microsoft/TypeScript#33912.
This leads to an error:
this.animal = a; // error!
// Type 'Animal' is not assignable to
// type 'InstanceType<this["AnimalConstructor"]>' 😟
If you prefer maintaining existing types, perhaps admitting your superiority over the compiler is the way forward. Given certainty that new this.AnimalConstructor()
explicitly corresponds to type
InstanceType<this['AnimalConstructor']>
, regardless of
this
variations in subclasses, assert this fact to reassure the compiler amidst uncertainty:
createAnimal() {
const ac = this.AnimalConstructor;
const a = new ac();
this.animal = a as InstanceType<this['AnimalConstructor']>; // okay
}
Or simply:
createAnimal() {
this.animal = new this.AnimalConstructor() as typeof this.animal; // okay
}
This approach facilitates progression albeit with some compromise in type safety, given the limitations of the compiler’s intelligence. Without extensive refactoring, consider explicitly structuring Population
as being generic in the instance type of AnimalConstructor
. Such a design empowers you to regulate when generically broadened and avoid complexities related to conditional types entirely:
export class Population<T extends Animal> {
constructor(public AnimalConstructor: new () => T) {
this.animal = new AnimalConstructor(); // ensure initialization
}
animal: T
createAnimal() {
this.animal = new this.AnimalConstructor(); // okay
}
Explore this code on TypeScript Playground