Home > Software engineering >  Right way to manage Arrays with intersecting types and narrow them back down
Right way to manage Arrays with intersecting types and narrow them back down

Time:04-07

When using arrays with different Types in typescript I tend to run into issues with properties that are not on all Types.

I ran into the same issue for different types of sections on a page, different roles of users with different properties and so on.

Here is an example with animals:

For example, if you have types Cat, Dog and Wolf:

export type Cat = {
    animal: 'CAT';
    legs: 4;
}
export type Dog = {
    animal: 'DOG',
    legs: 4,
    requiresWalks: true,
    walkDistancePerDayKm: 5
}
export type Wolf = {
    animal: 'WOLF',
    legs: 4,
    requiresWalks: true,
    walkDistancePerDayKm: 20
}
type Animal = Cat | Dog | Wolf;


const animals: Animal[] = getAnimals();
animals.forEach(animal => {
    // here i want to check if the animal requires a walk
    if (animal.requiresWalks) {
        // Property 'requiresWalks' does not exist on type 'Animal'. Property 'requiresWalks' does not exist on type 'Cat'.
        goForAWalkWith(animal)
    }
});
// The type "AnimalThatRequiresWalks" does not exist and i want to know how to implement it
goForAWalkWith(animal: AnimalThatRequiresWalks) {

}

As commented above, property requiresWalks can not be used to narrow down the type.

Also imagine we have 20 animals. I am having difficulties implementing types that may extend animals like "AnimalThatRequiresWalks" which may have multiple properties related to walking animals.

What is a clean implementation to join these types with a type "AnimalThatRequiresWalks" (with properties "requiresWalks true" and "walkDistancePerDayKm") and how can i properly narrow it down to "AnimalThatRequiresWalks"?

CodePudding user response:

You have two questions:

  1. How do you check requiresWalks to see if the animal requires a walk?

  2. How do you define the type for animal in goForAWalkWith?

Re #1: You test to see if the object has the property before you try to use it (this is a specific type of narrowing the handbook calls in operator narrowing):

animals.forEach(animal => {
    if ("requiresWalks" in animal && animal.requiresWalks) {
// −−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        goForAWalkWith(animal)
    }
});

Re #2, you can extract from the Animal union all types that are assignable to {requiresWalks: true} via the Extract utility type:

function goForAWalkWith(animal: Extract<Animal, {requiresWalks: true}>) {
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // ...
}

Extract<Animal, {requiresWalks: true}> is Dog | Wolf.

Playground link

You con't have to do that inline if you don't want to, you can define a type alias for it, then use the alias:

type AnimalThatRequiresWalks = Extract<Animal, {requiresWalks: true}>;
// ...
function goForAWalkWith(animal: AnimalThatRequiresWalks) {
    // ...
}

Playground link

  • Related