The correct method for handling arrays with overlapping types and narrowing them down again

When working with arrays containing different types in TypeScript, I often encounter issues with properties that are not present on all types.

The same challenge arises when dealing with various sections on a page, different user roles with varying properties, and so forth.

For instance, let's consider an example involving animals:

Suppose we have types for 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, the goal is to check if the animal requires a walk
    if (animal.requiresWalks) {
        // Error: 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 need guidance on how to set it up
goForAWalkWith(animal: AnimalThatRequiresWalks) {

}

As noted above, using the property requiresWalks to narrow down the type results in errors.

Moreover, when dealing with a larger number of animals, designing types that can extend animals—such as "AnimalThatRequiresWalks" with multiple properties related to walking animals—poses challenges.

How can I cleanly merge these types with "AnimalThatRequiresWalks" (having properties "requiresWalks true" and "walkDistancePerDayKm") and effectively narrow it down to "AnimalThatRequiresWalks"?

Answer №1

You have a couple of inquiries:

  1. How can you verify if an animal requires a walk by checking the requiresWalks property?

  2. What is the process for defining the type of animal in the function goForAWalkWith?

In response to #1: To determine if the object has the property before using it, you should check and test it. This tactic is known as in operator narrowing:

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

Regarding #2, you can isolate all types from the Animal union that are compatible with {requiresWalks: true} using the Extract utility type:

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

The result of

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

interface AnimalThatRequiresWalks { animal: string; requiresWalks: true; preferredWalkTerrain: "hills" | "paths" | "woods"; walkDistancePerDayKm: number; } export type Cat = { animal: "CAT"; legs: 4; }; export type Dog = AnimalThatRequiresWalks & { animal: "DOG"; legs: 4; walkDistancePerDayKm: 5; preferredWalkTerrain: "paths"; }; export type Wolf = AnimalThatRequiresWalks & { animal: "WOLF"; legs: 4; walkDistancePerDayKm: 20; preferredWalkTerrain: "woods"; } type Animal = Cat | Dog | Wolf; declare const animals: Animal[]; animals.forEach(animal => { if ("requiresWalks" in animal && animal.requiresWalks) { goForAWalkWith(animal) } }); function goForAWalkWith(animal: AnimalThatRequiresWalks) { console.log("Time for a walk with the " + animal.animal); }

Playground link

You may want to test this approach to gauge its usability. It's crucial not to overlook the AnimalThatRequiresWalks & part when specifying Dog, Wolf, etc., and note that the type of

AnimalThatRequiresWalks["animal"]
is a string, while inference results in a more specific type like "Cat" | "Dog" | "Wolf". The convenience factor plays a significant role here.

An alternative solution to avoid forgetting the initial AnimalThatRequiresWalks & declaration is to use a generic type for animals:

interface AnimalThatRequiresWalks {
    animal: string;
    requiresWalks: true;
    preferredWalkTerrain: "hills" | "paths" | "woods";
    walkDistancePerDayKm: number;
}

type AnimalType =
    R extends true
    ? AnimalThatRequiresWalks & Type
    : Type;

export type Cat = AnimalType;
export type Dog = AnimalType;
export type Wolf = AnimalType;
type Animal = Cat | Dog | Wolf;

declare const animals: Animal[];
animals.forEach(animal => {
    if ("requiresWalks" in animal && animal.requiresWalks) {
        goForAWalkWith(animal)
    }
});

function goForAWalkWith(animal: AnimalThatRequiresWalks) {
    console.log("Time for a walk with the " + animal.animal);
}

Playground link

Ultimately, selecting between these options depends on your project requirements and the development team's comfort level with each approach. Feel free to experiment and see which method works best for your specific case!

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the process of adding an m4v video to a create-next-app using typescript?

I encountered an issue with the following error: ./components/Hero.tsx:2:0 Module not found: Can't resolve '../media/HeroVideo1-Red-Compressed.m4v' 1 | import React, { useState } from 'react'; > 2 | import Video from '../ ...

Using Webpack 4 and React Router, when trying to navigate to a sub path,

I'm currently working on setting up a router for my page, but I've encountered a problem. import * as React from 'react'; import {Route, Router, Switch, Redirect} from 'react-router-dom'; import { createBrowserHistory } from ...

Determine the time difference between the beginning and ending times using TypeScript

Is there a way to calculate the difference between the start time and end time using the date pipe in Angular? this.startTime=this.datePipe.transform(new Date(), 'hh:mm'); this.endTime=this.datePipe.transform(new Date(), 'hh:mm'); The ...

What is the mechanism behind the widening of object literal types in Typescript inference?

I've been reading up on how typescript broadens inferred types but I'm still not entirely clear about what's happening here: type Def = { 'T': { status: 5, data: {r: 'm'}}, } function route<S extends keyof Def> ...

Tips for utilizing import alongside require in Javascript/Typescript

In my file named index.ts, I have the following code snippet: const start = () => {...} Now, in another file called app.ts, the code is as follows: const dotenv = require('dotenv'); dotenv.config(); const express = require('express' ...

Typescript: How to Ensure Tuple Type is Explicit and Not Combined

I have a code snippet that defines a person object and a function to get its values: const person = { name: "abc", age: 123, isHere: true }; const getPersonValues = () => { return [person.name, person.age, person.isHere]; }; const [n ...

Error encountered when using withRouter together with withStyles in Typescript on ComponentName

Building an SPA using React with Typescript and Material UI for the UI framework. Stuck on a recurring error across multiple files - TS2345 Typescript error: Argument of type 'ComponentType<Pick<ComponentProps & StylesProps & RouteCompo ...

Can anyone offer any suggestions for this issue with Angular? I've tried following a Mosh tutorial but it's

Just finished watching a video at around 1 hour and 35 minutes mark where they added the courses part. However, I encountered an error during compilation. ../src/app/app.component.html:2:1 - error NG8001: 'courses' is not recognized as an elemen ...

Tips for stopping </p> from breaking the line

<p>Available: </p><p style={{color:'green'}}>{props.active_count}</p><p>Unavailable: </p><p style={{color:'red'}}>{props.inactive_count}</p> https://i.sstatic.net/NQo5e.png I want the ou ...

Using TypeScript in the current class, transform a class member into a string

When converting a class member name to a string, I rely on the following function. However, in the example provided, we consistently need to specify the name of the Current Component. Is there a way to adjust the export function so that it always refers ...

Encountering TypeScript error 2345 when attempting to redefine a method on an Object Property

This question is related to Object Property method and having good inference of the function in TypeScript Fortunately, the code provided by @jcalz is working fine; const P = <T,>(x: T) => ({ "foo": <U,>(R: (x: T) => U) => ...

Create a class with additional attributes to support different types of options

I define a set of options represented by strings: export type Category = 'people' | 'projects' | 'topics' | 'tools' An index is declared as follows: interface Entry { ... } type IPostEntryIndex = { [name in Cate ...

What is the best way to find the clinic that matches the chosen province?

Whenever I try to choose a province from the dropdown menu in order to view clinics located within that province, an error 'TypeError: termSearch.toLowerCase is not a function' pops up. This part of the code seems confusing to me and any kind of ...

Create a rectangle on the canvas using the Fabric.js library in an Angular application

I am attempting to create a rectangle inside a canvas with a mouse click event, but I am encountering some issues. The canvas.on('selection:created') event is not firing as expected. Below is my code: let can = new fabric.Canvas('fabricCanv ...

The error code TS2474 (TS) indicates that in 'const' enum declarations, the member initializer must be a constant expression

Error code: export const enum JSDocTagName { Description = "desc", Identifier = "id", Definition = "meaning", } Implementing Angular 6 in conjunction with the .NET framework. ...

New Entry failing to appear in table after new record is inserted in CRUD Angular application

Working with Angular 13, I developed a basic CRUD application for managing employee data. Upon submitting new information, the createEmployee() service is executed and the data is displayed in the console. However, sometimes the newly created entry does no ...

Ensuring the accurate usage of key-value pairs in a returned object through type-checking

After generating a type definition for possible response bodies, I am looking to create a function that returns objects shaped as { code, body }, which are validated against the typing provided. My current solution looks like this: type Codes<Bodies> ...

Mismatch between generic types

When working with this code, I encounter a syntax error at m1 and m2. The error message states: Type 'T' is not assignable to Type 'boolean' or Type 'T' is not assignable to Type 'string' interface customMethod { ...

Check for the data attributes of MenuItem in the TextField's onChange event listener

Currently, I am facing a situation where I have a TextField in select mode with several MenuItems. My goal is to pass additional data while handling the TextField's onChange event. I had the idea of using data attributes on the MenuItems for this pur ...

How to transfer a parameter in Angular 2

I am currently facing a challenge in passing a value from the HTML file to my component and then incorporating it into my JSON feed URL. While I have successfully transferred the value to the component and displayed it in the HTML file, I am struggling to ...