I’m working on a TypeScript project where I’ve defined an abstract class WeatherProcessor that uses generics and abstract methods to fetch and process weather data. The class is designed to be extended by concrete implementations that specify the types for URL parameters and the fetched data. Here’s a simplified version of my abstract class and a concrete implementation:
type UnWrap<T extends (...args: any) => any>= Awaited<ReturnType<T>>
abstract class WeatherProcessor<
T extends Record<string, string>,
U extends { [K in keyof T]: any }, >{
abstract storageKey: string
abstract units: WeatherUnits & {language:GlobalLanguage}
abstract URLGenerate():Promise<T>
abstract groupFetch(): Promise<U & {$isFresh?: boolean}>;
abstract convertToAppData(input: UnWrap<WeatherProcessor<T, U>['interpreter']>): HomeWeather|undefined
interpreter(){
const data = this.groupFetch()
const comeOn = {"verylong": "yes"}
const b = dynamicB()
return {...data, "somethingPatcher": "123", "$isFresh": true, comeOn, b}
}
}
}
type WeatherUnits = { metric: boolean };
type GlobalLanguage = 'en' | 'es';
// The simplified HomeWeather type
interface HomeWeather {
city: string;
temperature: number;
units: string;
}
// Concrete implementation of WeatherProcessor
class SimpleWeatherProcessor extends WeatherProcessor<
{ city: string },
{ city: number }
> {
storageKey = 'simpleWeather';
units: WeatherUnits & {language: GlobalLanguage} = { metric: true, language: 'en' };
async URLGenerate(): Promise<{ city: string }> {
return { city: 'London' }; // Example: generates URL parameters for fetching weather
}
async groupFetch(): Promise<{ city: number; $isFresh?: boolean }> {
return { city: 20, $isFresh: true }; // Example: fetches weather data
}
convertToAppData( input: UnWrap<WeatherProcessor<{ city: string }, { city: number }>['interpreter']>): HomeWeather | undefined {
if (!input) return undefined;
// Assuming 'metric' means Celsius
const temperatureUnit = this.units.metric ? 'C' : 'F';
return {
city: 'Example City',
temperature: input.city,
units: temperatureUnit,
};
}
>
function dynamicB() {
return {
"I can be any thing" : "anything"
}
}
My goal is to simplify the usage of UnWrap<WeatherProcessor<T, U>[‘interpreter’]> in child classes, specifically in the convertToAppData method, as I currently need to manually specify the generic parameters each time, which is quite verbose and cumbersome:
input: UnWrap<WeatherProcessor<{ city: string }, { city: number }>['interpreter']>
I’m looking for a way to simplify or omit the explicit typing of UnWrap<WeatherProcessor<T, U>[‘interpreter’]> every time, ideally leveraging TypeScript’s type inference or any other mechanism to make the child class implementation cleaner and less verbose.
Is there a pattern or TypeScript feature that can help simplify this pattern, reducing the redundancy and manual typing involved in extending abstract classes with complex generic types?
I currently need to manually specify the generic parameters each time
2
Answers
It would be great if subclasses would contextually type their method parameter types from their superclasses. There is a longstanding suggestion for this feature, at microsoft/TypeScript#23911, but for now it’s not part of the language.
That means you are required to write out the type for
input
for each subclass. There’s no easy shortcut, not at least without some refactoring of the runtime part of your code, which I doubt you want to do.Since you have to write the type, the best I can imagine is coming up with a less cumbersome way to do it. One approach is to make a type
Input<T>
that takes a type parameter corresponding to the instance type of the currentWeatherProcessor
class, and determines the appropriateinput
type:Then, you can use polymorphic
this
to refer to the type of the current class without needing to write out the type arguments. That is, useInput<this>
:and
And you can inspect that inside this
convertToAppData()
implementation, theinput
parameter has the expected properties:Looks good. It would be wonderful if you could just write
(input)
, but(input: Input<this>)
isn’t so terrible.Playground link to code
Perhaps this is not the answer you’re looking for but bear with me.
Why do you need inheritance… or classes at all for that matter?
From what I can discern, it appears this is an application to pull weather data from some source and turn it’s response into something usable within the app. Though it could be something that provides weather data… I’m not 100% sure, there’s a lot of context elided.
Whilst it feels quite good to play with the type system and create beautifully expressive types, the question I’d ask at this point is: Are you doing this for your benefit, or someone else’s?
If the answer to that question is the latter, I’d probably aim to make your code simpler. Code gets read far more times than it gets written. If this is purely an exercise to learn inheritance or TypeScript or something else, I’d go with what @jcalz has mentioned above but if this is going to be used, I’d question what the actual need for inheritance is. Perhaps there’s some magic thing I’m not seeing but from what I can see it just adds unnecessary complexity. If something blows up in one class that is inherited by another class, which is inherited by another… you get the point. Debugging can become real difficult real fast.
Ontop of that, newcomers to the codebase will take 1 look at that then decide to go for a 5 minute break first.
That being said, many people write code this way so it’s helpful to at least know. Maybe there is also some hidden benefit to doing it your way… again I have very little context.
The way I’d go about doing this is as follows. Of course, I don’t have the full picture so don’t expect this to make 100% sense in the context of your specific application. Hopefully this gets the general gist, though.
One thing to remember about TypeScript is that it’s essentially just a really complicated linter for JavaScript. In JavaScript, we don’t have classes. We have functions masquerading as classes.
The ramifications to taking the object oriented approach are things like:
type UnWrap<T extends (...args: any) => any>= Awaited<ReturnType<T>>
and why do I need this to handle a network response?".I’m not trying to rip your code for the sake of ripping your code and I hope that my intentions were clear via written medium. Nice work 🙂