Issue
Dealing with generic classes can be tricky, especially when defining the generic parameter T
for each instance. While specifying the type T
during the creation of a generic class instance is possible, it is often unnecessary due to the constructor signature providing TypeScript with the necessary information about the relationship between the arguments and the generic type T
.
By utilizing constructors like
constructor(entity: T, collectionName: string)
, TypeScript effectively assigns the type of the variable
entity
as the generic type
T
for that specific instance. Consequently, creating instances such as
const myObj = new ConcreteRepository(myEntity, myCollectionName);
allows TypeScript to determine the type of
myObj
as
ConcreteRepository<typeof myEntity>
.
However, when it comes to static properties, they belong to the class itself and are shared among all instances rather than being dependent on the current instance's generic type T
. This can lead to confusion and inefficiencies in managing static variables tied to generic types.
Resolution 1
To address this concern, transforming the static variable conn
into an instance variable within each instance of a class ensures that conn
corresponds to the entity type associated with that instance. Each instance sets its own connection using this.conn
in the constructor.
export abstract class AbstractRepository<T extends Typegoose> {
protected conn: SessionDb<T>;
protected model: Model<InstanceType<T>, {}>;
constructor(entity: T, collectionName: string){
this.conn = SessionDb.connect(entity);
this.model = this.conn.getModel(collectionName);
}
}
This design enables implementing the base class by either generic classes or those bound to a specific value of T
.
Resolution 2
An alternative solution involves making use of an abstract class like AbstractRepository
, which cannot be directly instantiated but must be extended by other classes. For instances where extending classes cater to specific entity types, each class can have its unique static conn
variable.
The implementation might resemble the following structure:
export abstract class AbstractRepository<T extends Typegoose> {
protected model: Model<InstanceType<T>, {}>;
constructor(entity: T, collectionName: string){
this.model = this.getConnection().getModel(collectionName);
}
abstract getConnection(): SessionDb<T>;
}
export class SpecificRepository extends AbstractRepository<SpecificType> {
private static conn: SessionDb<SpecificType>;
getConnection(): SessionDb<SpecificType> {
if( SpecificRepository.conn === undefined) {
// additional logic required based on entity information
SpecificRepository.conn = SessionDb.connect(entity);
}
return SpecificRepository.conn;
}
}
In this case, the base class mandates that each subclass implements a method getConnection
returning a matching SessionDb
for type T
. Although the actual implementation is left to the subclasses, the base class can safely utilize this method when needed.