letmejustfixthat
letmejustfixthat

Reputation: 3459

Declare a generic Typescript array with a common base interface as types

I'd like to declare an Array of items with a common base interface, but can't figure out how.

I have a base interface and several child interfaces extending the base:

interface Animal {
  name: string;
  birthdate: Date;
}

interface Bird extends Animal {
  featherColor: string;
}

interface Dog extends Animal {
  furColor: string;
}

I would like to do something like this:

interface AnimalList {
  animals: Array<? extends Animal>
}

// define an example
const list: AnimalList = {
  animals: [
    {name: 'Bobby', birthdate: new Date(), furColor: 'brown'},
    {name: 'Bobby', birthdate: new Date(), featherColor: 'green'},
  ]
}

How can I declare an array like that? Please note: I cannot use classes as the definition of the interfaces will be done with an OpenApi generator.

I tried Animal[], but that will produce:

TS2322: Type '{ name: string; birthdate: Date; furColor: string; }' is not assignable to type 'Animal'. 
Object literal may only specify known properties, and 'furColor' does not exist in type 'Animal'.

Upvotes: 0

Views: 666

Answers (1)

You have two options:

interface Animal {
    name: string;
    birthdate: Date;
}

interface Bird extends Animal {
    featherColor: string;
}

interface Dog extends Animal {
    furColor: string;
}

interface AnimalList<T extends Animal> {
    animals: Array<T>
}


const list = <T extends Animal>(arg: AnimalList<T>) => arg

/**
 * Fisrt, accepts all types which extends Animal
 */
const result = list({
    animals: [
        { name: 'Bobby', birthdate: new Date(), furColor: 'brown' },
        { name: 'Bobby', birthdate: new Date(), featherColor: 'green' },
    ]
})


/**
 * Second, if you want to define upfront allowed types
 */
const list2: AnimalList<Bird | Dog> = {
    animals: [
        { name: 'Bobby', birthdate: new Date(), furColor: 'brown' },
        { name: 'Bobby', birthdate: new Date(), featherColor: 'green' },
    ]
}

As you see, you may use function in order to infer the type and you can use union type

Playground

UPDATE

As for second approach

As you might have noticed, you are unable to add animal which is neither Bird nor Dog

const list2: AnimalList<Bird | Dog> = {
    animals: [
        { name: 'Bobby', birthdate: new Date(), furColor: 'brown' },
        { name: 'Bobby', birthdate: new Date(), featherColor: 'green' },
        { name: 'Bobby', birthdate: new Date(), fname: 'green' }, // error
    ]
}

If you want to filter birds from the array, you can use typeguards:

const isBird = (animal: Animal): animal is Bird =>
    Object.prototype.hasOwnProperty.call(animal, 'featherColor')

const getAnimals = list2.animals.filter(isBird) // Bird[]

It is not clear for me what you mean under

regarding maintainability, bad habit, isn't it

Upvotes: 1

Related Questions