I am currently dealing with a situation where I have an abstract class that contains generic types for internal purposes only:
public abstract class ParentClass<T, U> {
// Some common code to prevent code duplication in child classes
protected abstract adaptThese(...things: Array<MyType>): Array<T>
protected abstract adaptThose(...things: Array<MyType>): Array<U>
// Other non-generic abstract methods
}
Below is an example of a child class that extends the abstract class:
public class ChildClass extends ParentClass<FirstType, SecondType> {
protected adaptThese(...things: Array<MyType>): Array<FirstType> {
//...
}
protected adaptThose(...things: Array<MyType>): Array<SecondType> {
//...
}
// Implementations of the other abstract methods...
}
While my code is functioning properly, all references to ParentClass
must include ParentClass<any, any>
. Since the generic types are solely used internally (in protected
methods), I'm exploring ways to remove the generics from the parent class while still ensuring type safety when the children use <T>
and <U>
.
The initial purpose of introducing these generics was to enforce child classes to provide adaptations from MyType
to both T
and U
, which are specific to libraries and differ from MyType
. This design allows flexibility for swapping out underlying libraries without impacting code outside this hierarchy. One potential solution could involve creating an inner adapter object containing the generics.
One approach I considered was adding a parameter of type MyAdapter<any, any>
to the constructor of ParentClass
, but I couldn't find a way to make any
transform into generic types ensuring type-safety in the children.
It's worth noting that ParentClass
doesn't reference the generic methods within itself, they exist solely to mandate child implementations. Perhaps there's a viable interface design approach to achieve this?
In summary: Is there a way to compel children to implement type-safe methods without declaring the types in the parent class signature?
To illustrate further, here's a simplified example showcasing how the type-dependencies are segregated:
Within ParentClass
, you might come across a method like this:
// Sets an array of things for a specific key (label)
setThingsFor(label: string) {
const thingsForLabel: Array<MyType> = myConfiguration.get(label);
// Perform calculations, apply filters based on configuration
this.setThings(thingsForDisplay);
}
// Abstract method to set an array of things for the feature
abstract setThings(things: Array<MyType>): void;
And in a child class:
public class CoolLibraryImplForParentClass extends ParentClass<CoolType, RadType> {
setThings(things: Array<MyType>): void {
// Use "things" to determine library-specific configuration
coolLibraryService.initialize(this.adaptThose(things)); // RadType
coolLibraryService.doThings(this.adaptThese(things)); // CoolType
}
// Additional generic methods and CoolLibrary-specific operations
}
There are several similar methods pertaining to tooling or retrieving data from the library's services requiring one of the two types mentioned above.
Considering the similarities between the code for each library, I opted for a parent-child class structure to handle project-facing logic and library-dependent specifics.
Another idea under consideration is creating a project-facing service class and providing it with a generically-typed library-integrating service. However, I am still eager to explore solutions for enforcing type-safe methods in child classes without explicitly declaring types in the parent class.