skip to Main Content

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


  1. 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 current WeatherProcessor class, and determines the appropriate input type:

    type Input<T extends WeatherProcessor<any, any>> =
      Awaited<ReturnType<T['interpreter']>>
    

    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, use Input<this>:

    abstract class WeatherProcessor<
      T extends Record<string, string>,
      U extends { [K in keyof T]: any },> {
      ⋮
      abstract convertToAppData(input: Input<this>): HomeWeather | undefined
      ⋮
    }
    

    and

    class SimpleWeatherProcessor extends WeatherProcessor<
      { city: string },
      { city: number }
    > {
    
      ⋮
    
      convertToAppData(input: Input<this>): 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,
        };
      }
    }
    

    And you can inspect that inside this convertToAppData() implementation, the input parameter has the expected properties:

    // input.$isFresh // (property) $isFresh?: boolean | undefined
    // input.city // (property) city: number
    

    Looks good. It would be wonderful if you could just write (input), but (input: Input<this>) isn’t so terrible.

    Playground link to code

    Login or Signup to reply.
  2. 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.

    // Request sent
    type AppRequest = {
        city: string,
    }
    
    // Response received 
    type AppResponse = {
        city: number,
        temperature: number,
        $isFresh: boolean,
    }
    
    // Data converted to format our app expects
    type Converted = {
        city: string,
        temperature: number,
        units: "C" | "F"
    }
    
    // Data we know about city before sending the request.. This might be covered in the response 
    // but it's impossible to know without seeing the shape of the data
    type CityMetaData = {
        canonnicalName: string,
        usesMetricSystem: boolean,
    }
    let cities: Map<number, CityMetaData> = new Map();
    cities.set(1, {
        canonnicalName: "Manchester",
        usesMetricSystem: true
    });
    
    // and if you want it to be constant...
    cities = Object.freeze(cities);
    
    // This is the function that would replace your need for classes and inheritance entirely
    async function runTheThing(_paramsRequired: any): Promise<Converted | null> {
        try {
            const url: AppRequest = await generateURL();
            const response: AppResponse = await groupFetch(url);
            const cityMeta: CityMetaData | undefined = cities.get(response.city);
            if (!cityMeta) {
                throw new Error("handle me properly...")
            }
            return convertToAppData(cityMeta, response);
        } catch(e) {
            // handle errors properly
            return null;
        }
    }
    
    async function generateURL(): Promise<AppRequest> {
        // ..
        return {
            city: "Manchester"
        }
    }
    
    
    async function groupFetch(_req: AppRequest): Promise<AppResponse> {
        return {
            city: 1,
            $isFresh: true,
            temperature: 16,
        }
    }
    
    function convertToAppData(cityMeta: CityMetaData, res: AppResponse): Converted {
        return {
            city: cityMeta.canonnicalName,
            temperature: res.temperature,
            units: cityMeta.usesMetricSystem ? "C" : "F"
        }
    }
    

    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:

    • Higher memory overhead (and subsequently longer/more frequent GC times, dependent on how the classes are used)
    • Readability. "What the heck is type UnWrap<T extends (...args: any) => any>= Awaited<ReturnType<T>> and why do I need this to handle a network response?".
    • Code surface area. Would you rather look at 195 different files, one for each country, to dig through and make one implementation change in every file; or would you rather change just 1 function, maybe adding an additional function for the weird requirements of the response from Liechtenstein, specifically.
    • Extensibility. If the shape of the data changes, how many places would you have to update in your current code?
    • Debugging. If you’re taking the inheritance approach, I can only imagine that you’re inheriting these classes into something else, somewhere down the line. What if an uncaught exception happens way down in the base class. Have you tried reading one of those stack traces… let me tell you, they can be a pain.
    • Incoherent transpiled output. TypeScript will do some weird and wonderful things during transpilation, especially if you do things like backfill features for older runtimes. If you’re ever in a situation where you have to comb through the resultant JavaScript (again, not fun), you’d be surprised at just how different the JavaScript can and will be from the inputted TypeScript. This is a non-issue in most cases but it’s always the ones you don’t expect. My general take is to try and keep it as simple as possible and only reach for the top-shelf when the use-case really requires it. Async iterators, Proxies etc.

    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 🙂

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search