How to pass a child Interface to a parent class?

Viewed 166

I have this:

LocationController.ts

import {GenericController} from './_genericController';

interface Response {
    id : number,
    code: string,
    name: string,
    type: string,
    long: number,
    lat: number
}
const fields = ['code','name','type','long','lat'];

class LocationController extends GenericController{
    tableName:string = 'location';
    fields:Array<any> = fields;
}

const locationController = new LocationController();

const get = async (req, res) => {
    await locationController._get(req, res);
}

export {get};

GenericController.ts


interface Response {
    id : number
}

export class GenericController{
    
    tableName:string = '';
    fields:Array<any> = [];

    _get = async (req, res) => {
        try{
            const id = req.body['id'];
            const send = async () => {
                const resp : Array<Response> = await db(this.tableName).select(this.fields).where('id', id)
                if (resp[0] === undefined) {
                    // some error handling
                }
                res.status(status.success).json(resp[0]);
            }
            await send();
        }catch (error){
            // some error handling
        }
    }    
}

What I want to do is to pass the Response interface from LocationController to the GenericController parent, so that the response is typed accurately depending on how the child class has defined it. Clearly it doesn't work like this since the interface is defined outside of the class so the parent has no idea about the Response interface in the LocationController.ts file.

I've tried passing interface as an argument in the constructor, that doesn't work. So is there a way I can make this happen? I feel like I'm missing something really simple.

1 Answers

Typically, generics are used in a situation like this. Here's how I'd do it:

interface Response {
  id: number;
}

// Note the generic parameter <R extends Response>
export class GenericController<R extends Response> {
  tableName: string = "";
  fields: Array<any> = [];

  _get = async (req, res) => {
    try {
      const id = req.body["id"];
      const send = async () => {
        // The array is now properly typed. You don't know the exact type,
        // but you do know the constraint - R is some type of `Response`
        let resp: Array<R> = await db(this.tableName).select(this.fields).where("id", id);

        if (resp[0] === undefined) {
          // some error handling
        }

        res.status(status.success).json(resp[0]);
      };
      await send();
    } catch (error) {
      // some error handling
    }
  };
}
import { GenericController } from "./_genericController";

interface Response {
  id: number;
  code: string;
  name: string;
  type: string;
  long: number;
  lat: number;
}

const fields = ["code", "name", "type", "long", "lat"];

// Here we tell the GenericController exactly what type of Response it's going to get
class LocationController extends GenericController<Response> {
  tableName: string = "location";
  fields: Array<any> = fields;
}

const locationController = new LocationController();

const get = async (req, res) => {
  await locationController._get(req, res);
};

export { get };

If this is not enough and you wish to somehow know the exact response type you're going to get, I believe the only way is a manual check. For example:

import { LocationResponse } from './locationController';

// ... stuff

    // Manual runtime type check
    if (this.tableName === 'location') {
      // Manual cast
      resp = resp as Array<LocationResponse>
    }

// ...

You could also check the form of resp[0] (if (resp[0].hasOwnProperty('code')) { ... }) and cast accordingly. There are also nicer ways to write this, but the basic idea remains the same.

Generally, a properly written class should be unaware of any classes that inherit from it. Putting child-class-specific logic into your generic controller is a code smell. Though as always, it all depends on a particular situation.

Related