Home > Blockchain >  Is type inference based off the shape of an object possible in typescript?
Is type inference based off the shape of an object possible in typescript?

Time:10-20

Lets say I have 3 Interfaces:

interface Animal {
  height: number
  name: string
}

interface Dog extends Animal {
  numberOfTeeth: number
  hasTail: boolean
}

interface Cat extends Animal {
  sizeOfPaw: number
}

and lets say I query an api like this:

function getAnimal(id:string, animalType:string) : Animal {
  const res = await (axios.get(`http://animalapi.com/${animalType}/${id}`));
  return res.data; // Could be a cat, could be a dog, based on animal type
}

If I try to do:

const animal:Dog = getAnimal("1", "dog"); // Would it be possible to not need to pass the string dog? Perhaps store the "dog" value within the interface and pass it from there?

I get errors that 'Animal is missing the following types', followed by the types that are in Dog.

Is it possible to be able to obtain both Dog and Cat from the same function, without having to duplicate the call?

CodePudding user response:

Is type inference based off the shape of an object possible in typescript?

Yup, this is called type narrowing, and you can do it with a discriminated union that joins the Dog and Cat shapes but gives each shape a unique "tag" that distinguishes it from the others (this might be a property like id, type, or whatever you want to call it):

interface AnimalBase {
  height: number
  name: string
}

interface Dog extends AnimalBase {
  type: 'dog';
  numberOfTeeth: number
  hasTail: boolean
}

interface Cat extends AnimalBase {
  type: 'cat';
  sizeOfPaw: number
}

type Animal = Dog | Cat;

Now, when you go to create an Animal, TypeScript will expect you to identify the specific shape by specifying the type property. Then you can pass along animal.type wherever you want since it's a string literal:

const dog: Animal = {
  type: 'dog',
  numberOfTeeth: 100,
  hasTail: true,
  height: 100,
  name: 'Roofus'
};

getAnimal("1", animal.type)

This pattern has the added benefit of allowing you to narrow a generic Animal type to a specific sub-type. For example, you can use it to write a type predicate to check if an animal is of a particular constituent type:

const isDog = (animal: Animal): animal is Dog => {
  return animal.type === 'dog';
}

const dog: Animal = {
  type: 'dog',
  numberOfTeeth: 100,
  hasTail: true,
  height: 100,
  name: 'Roofus'
};

// here, TypeScript knows that animal is now of type Dog
if (isDog(dog)) {
  // myAnimal shape is now Dog
}

Without discriminated unions, you'd need to do a type assertion and check a property that you know is absolutely unique to dogs (maybe hasTail, but that's true for cats too):

const isDog = (animal: Animal): animal is Dog => {
  return typeof (animal as Dog).numberOfTeeth !== 'undefined';
}
  • Related