0

I want to implement a generic method that returns a value containing depending on the interface the object has. However cannot figure out how to do it, I've looked at typeof and interfaceof but not sure how to implement it to satisfy below logic.

e.g.

export interface Animal {
  name: string;
}

export interface Vehicle {
  model: string;
}

getDesignation<T>(data: T): string {

// if data has ANIMAL interface on it
// return data.name;

// if data has VEHICLE interface on it
// return data.model;
}

so example of working code:

const dog: Animal = {
  name: 'Rex'
};

const ferrari: Vehicle = {
  model: 'Spider'
};

console.log(getDesignation<Vehicle>(ferrari)); /// prints Spider
console.log(getDesignation<Animal>(dog)  /// prints Rex
Aeseir
  • 7,754
  • 10
  • 58
  • 107
  • 4
    Typescript's types (including interfaces) don't exist at runtime, so you have to think how you would solve this in Javascript and then write that. Typescript then only checks that what you wrote is type-safe. Something like `if('name' in data) { ... } else if('model' in data) { ... }` might do what you want. – kaya3 Feb 20 '21 at 01:58

2 Answers2

2

As @kaya3 said, typescript types exist at compile time only so you cannot check against an interface at runtime. You need to instead look at an object and figure out if it fulfills the interface that you want by using type guards.

The keyword in is a built-in type guard, so you can do this:

const getDesignation = (data: Animal | Vehicle): string => {
    if ('name' in data) {
        return data.name;
    } else {
        return data.model;
    }
}

I'm actually kind of surprised that this compiles because you could have a Vehicle with {name: number}, but it seems to work. You could have better type-safety with an XOR instead of Animal | Vehicle.

For a more complex use case, you can create your own user-defined type guards.

If it has a string property name, we know it's an Animal:

const isAnimal = <T extends {}>(value: T & {name?: any}): value is T & Animal => {
    return typeof value.name === "string";
}

If it has a string property model, we know it's a Vehicle:

const isVehicle = <T extends {}>(value: T & {model?: any}): value is T & Vehicle => {
    return typeof value.model === "string";
}

You can use these functions to guard the type of a variable:

const getDesignation = (data: Animal | Vehicle): string => {
    if (isAnimal(data)) {
        return data.name;
    } else {
        return data.model;
    }
}

Playground Link

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
1

One slightly different way to approach this is to introduce a new interface for every interface you want to use this designation logic on:

interface HasDesignation {
    getDesignation: () => string
}


interface Animal extends HasDesignation {
  name: string;
}

interface Vehicle extends HasDesignation {
  model: string;
}

Now you can construct instances like this:

const createAnimal = (name: string): Animal => ({
    name,
    getDesignation() { return this.name }
})

const createVehicle = (model: string): Vehicle => ({
    model,
    getDesignation() { return this.model }
})

And getDesignation becomes very simple:

const getDesignation = <T extends HasDesignation>(data: T): string => data.getDesignation()`

Playground