0

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'.
1
  • That was my first approach, yes. But that will produce an 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'. Commented May 25, 2021 at 8:10

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

2 Comments

The second approach is, regarding maintainability, bad habit, isn't it? It works indeed, but seems error prone if there are a lot of different interfaces and not like two or three. However, I don't understand what list does - it seems redundant?
@letmejustfixthat in this particular case list is redundant. I just wanted to show you different approaches.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.