Creating a custom function to compute object attributes for an object that may contain optional properties

I have a dataset containing information for a variety of dog breeds, and I am looking to compute the average weight of certain breeds. Each record in the dataset represents a different breed of dog along with their respective weights. The data includes five core breeds that are always present and two additional breeds that may or may not appear:

// TypeScript

type dogSample = {
    // 5 core breeds that always appear
    bulldog: number,
    poodle: number,
    pug: number,
    chihuahua: number,
    boxer: number,
    // 2 additional that sometimes appear
    dalmatian?: number,
    rottweiler?: number, // rottweiler only appears if dalmatian does
    // other irrelevant properties
    foo: string,
    bar: boolean,
    baz: number
    // and possibly many other unrelated properties
}

The objective is to calculate the average weight of all the dogs based on these specific properties. To achieve this, three functions have been created:

const calcMeanCoreFive = (obj: dogSample) =>
  (obj.bulldog + obj.poodle + obj.pug + obj.chihuahua + obj.boxer) / 5;

const calcMeanCoreFivePlusDalmatian = (obj: Required<dogSample>) =>
  (obj.bulldog +
    obj.poodle +
    obj.pug +
    obj.chihuahua +
    obj.boxer +
    obj.dalmatian) /
  6;

const calcMeanCoreFivePlusDalmatianPlusRottw = (obj: Required<dogSample>) =>
  (obj.bulldog +
    obj.poodle +
    obj.pug +
    obj.chihuahua +
    obj.boxer +
    obj.dalmatian +
    obj.rottweiler) /
  7;

Finally, there is a master function that encapsulates all three versions:

const calcMeanDogSample = (obj: dogSample, nBreeds: 5 | 6 | 7) =>
  nBreeds === 5
    ? calcMeanCoreFive(obj)
    : nBreeds === 6
    ? calcMeanCoreFivePlusDalmatian(obj)
//                                   ^
    : calcMeanCoreFivePlusDalmatianPlusRottw(obj);
//                                            ^
// argument of type 'dogSample' is not assignable to parameter of type 'Required<dogSample>'

Attempted Solution

To address the issue, an attempt was made to modify the typing within calcMeanDogSample() using Required<dogSample>:

const calcMeanDogSample2 = (obj: Required<dogSample>, nBreeds: 5 | 6 | 7) =>
  nBreeds === 5
    ? calcMeanCoreFive(obj)
    : nBreeds === 6
    ? calcMeanCoreFivePlusDalmatian(obj)
    : calcMeanCoreFivePlusDalmatianPlusRottw(obj);

This adjustment resolved the error within the function's definition but caused issues when attempting to call calcMeanDogSample2() with an object of type dogSample:

const someDogSample = {
  bulldog: 24,
  poodle: 33,
  pug: 21.3,
  chihuahua: 7,
  boxer: 24,
  dalmatian: 20,
  foo: "abcd",
  bar: false,
  baz: 123,
} as dogSample;

calcMeanDogSample2(someDogSample, 6);
//                     ^
// Argument of type 'dogSample' is not assignable to parameter of type // 'Required<dogSample>'.
//   Types of property 'dalmatian' are incompatible.
//     Type 'number | undefined' is not assignable to type 'number'.
//       Type 'undefined' is not assignable to type 'number'.ts(2345)

Question

Is there an alternative way to define the typing for calcMeanDogSample() to resolve this issue?


Code can be tested on the TS playground

Answer №1

Instead of simply adapting your current algorithm to meet TypeScript's type safety standards, I recommend refactoring the approach to achieve a more universal solution:

const calculateMeanDogSample = (obj: DogSample) => {
  let sum = obj.bulldog + obj.poodle + obj.pug + obj.chihuahua + obj.boxer;
  let nBreeds = 5;
  for (let v of [obj.dalmatian, obj.rottweiler]) {
    if (typeof v === "number") {
      sum += v;
      nBreeds++;
    }
  }
  return sum / nBreeds;
}

To calculate the mean value, we divide the total sum of the relevant properties by the count of breeds (nBreeds). The five primary breed properties are mandatory, while the optional dalmatian and rottweiler ones contribute only if present.

You can verify this behavior with an example like this:

const someDogSample = {
  bulldog: 24,
  poodle: 33,
  pug: 21.3,
  chihuahua: 7,
  boxer: 24,
  dalmatian: 20,
  foo: "abcd",
  bar: false,
  baz: 123,
};

console.log(calculateMeanDogSample(someDogSample)); // 21.55

An even more streamlined refactoring option involves converting keys to potentially defined numbers, removing undefined values, and computing their mean:

const mean = (x: number[]) => x.reduce((a, v) => a + v, 0) / x.length;
const calculateMeanDogSample2 = (obj: DogSample) => mean(
  (["bulldog", "poodle", "pug", "chihuahua", "boxer", "dalmatian", "rottweiler"] as const)
    .map(k => obj[k])
    .filter((v): v is number => typeof v === "number")
);
console.log(calculateMeanDogSample2(someDogSample)); // 21.55

Playground link for code testing

Answer №2

The issue arises from the lack of connection between the nBreeds and the input obj within the type system.

Fortunately, TypeScript provides a solution by allowing you to notify the compiler that these properties do indeed exist in your data if they can satisfy a conditional check: this is known as a user-defined type guard with a return type referred to as a type predicate. Below is an example demonstrating its usage with your data, eliminating the need for the nBreeds argument!

You may also find the utility types Required<Type> and Pick<Type, Keys> useful.

type DogSample = {
  bulldog: number;
  poodle: number;
  pug: number;
  chihuahua: number;
  boxer: number;
  dalmatian?: number;
  rottweiler?: number;
  // other irrelevant properties
  foo: string;
  bar: boolean;
  baz: number;
};

// This is called a user-defined type guard (and the return type is called a type predicate)
function sampleIncludesBreed <T extends DogSample, K extends keyof DogSample>(
  obj: T,
  breed: K,
): obj is T & Required<Pick<DogSample, K>> {
  return typeof (obj as Required<DogSample>)[breed] === 'number';
}

const calcMeanCoreFive = (obj: DogSample) =>
  (obj.bulldog + obj.poodle + obj.pug + obj.chihuahua + obj.boxer) / 5;

const calcMeanCoreFivePlusDalmatian = (obj: (
  DogSample
  & Required<Pick<DogSample, 'dalmatian'>>
)) =>
  (obj.bulldog +
    obj.poodle +
    obj.pug +
    obj.chihuahua +
    obj.boxer +
    obj.dalmatian) /
  6;

const calcMeanCoreFivePlusDalmatianPlusRottw = (obj: (
  DogSample
  & Required<Pick<DogSample, 'dalmatian'>>
  & Required<Pick<DogSample, 'rottweiler'>>
)) =>
  (obj.bulldog +
    obj.poodle +
    obj.pug +
    obj.chihuahua +
    obj.boxer +
    obj.dalmatian +
    obj.rottweiler) /
  7;

const calcMeanDogSample = (obj: DogSample) => {
  if (sampleIncludesBreed(obj, 'dalmatian')) {
    return sampleIncludesBreed(obj, 'rottweiler')
      ? calcMeanCoreFivePlusDalmatianPlusRottw(obj) /*
                                               ^^^
      This type is: (parameter) obj: DogSample & Required<Pick<DogSample, "dalmatian">> & Required<Pick<DogSample, "rottweiler">> */
      : calcMeanCoreFivePlusDalmatian(obj); /*
                                      ^^^
      This type is: (parameter) obj: DogSample & Required<Pick<DogSample, "dalmatian">> */
  }
  return calcMeanCoreFive(obj); /*
                          ^^^
  This type is: (parameter) obj: DogSample */
};

const someDogSample = {
  bulldog: 24,
  poodle: 33,
  pug: 21.3,
  chihuahua: 7,
  boxer: 24,
  dalmatian: 20,
  foo: "abcd",
  bar: false,
  baz: 123,
} as DogSample;

calcMeanDogSample(someDogSample);

On a different note unrelated to your query but centered around dogs and data: You might enjoy exploring the API provided at dog.ceo.

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

Ng2-table experiencing compatibility issues with the most recent version of Angular2

Currently, my application is built using Angular2 and I am interested in integrating ng2-table into one of my components. You can find more information about ng2-Table on Github by visiting this link. I encountered an error while trying to implement this ...

Typescript input event

I need help implementing an on change event when a file is selected from an input(file) element. What I want is for the event to set a textbox to display the name of the selected file. Unfortunately, I haven't been able to find a clear example or figu ...

What is the best way to implement debouncing for an editor value that is controlled by the parent component?

Custom Editor Component import Editor from '@monaco-editor/react'; import { useDebounce } from './useDebounce'; import { useEffect, useState } from 'react'; type Props = { code: string; onChange: (code: string) => void ...

Sending variables to imported files using Typescript

Couldn't find it in the documentation. My question is, does TypeScript have a similar feature to this? file1.js module.exports = (service) => { service.doSomeCoolStuff(); } file2.js const file2 = require('./file2')(service) I need ...

Exploring the variance between inlineSourceMap and inlineSources within the TypeScript compiler options

According to the documentation: --inlineSourceMap and inlineSources command line options: --inlineSourceMap will include source map files within the generated .js files rather than in a separate .js.map file. --inlineSources allows for the source .t ...

How can I retrieve the SID received in a different tab using MSAL.js?

I have successfully integrated MSAL into a client-side library, and things are going smoothly so far. My next goal is to enable Single Sign-On (SSO) by following the instructions provided in the documentation at https://learn.microsoft.com/en-us/azure/act ...

Utilizing moment.js in conjunction with typescript and the module setting of "amd"

Attempting to utilize moment.js with TypeScript 2.1.5 has been a bit of a challenge for me. I went ahead and installed moment using npm : npm install moment --save-dev The d.ts file is already included with moment.js, so no need to install via @typings ...

Issue with calling jqPlot function in jQuery causing malfunction

I am perplexed as to why the initial code functions correctly while the second code does not. <a data-role="button" href="#page2" type="button" onClick="drawChart([[["Test1",6],["Test2",5],["Test3",2]]])"> <img src="icons/page2.png" inline style ...

Tips for closing a Modal Dialog in Vue3 using a different component, such as PimeVue

I'm currently working on implementing a Dialog component and integrating it into another component. I have managed to open the Dialog when a button is clicked, but I am facing difficulty in figuring out how to close it. Below are the details of the co ...

In order to load an ES module, specify the "type" as "module" in the package.json file or utilize the .mjs extension

I attempted to run this vscode extension repository on my desktop. After cloning it locally, I ran npm install Upon pressing f5 in the vscode editor, an error occurred: Process exited with code 1 (node:1404) Warning: To load an ES module, set "type": "mo ...

What methods are typically used for testing functions that return HTTP observables?

My TypeScript project needs to be deployed as a JS NPM package, and it includes http requests using rxjs ajax functions. I now want to write tests for these methods. One of the methods in question looks like this (simplified!): getAllUsers(): Observable& ...

What is the best way to assign table rows to various interfaces in typescript?

Assuming I have the interfaces provided below: export interface IUserRow { id: string, state: string, email: string, } export interface ITableRow { id: string, [key: string]: any; } export type Rows = ITableRow | IUserRow; // additio ...

Ensuring Input Validity in Angular4 and Symfony3

Currently, I am utilizing angular 4 and symfony3. In my application, there is a textarea that is required. However, when I only press enter (code 13) in this textarea without entering any other character, the form gets submitted. How can I prevent this spe ...

Replace deprecated TypedUseSelectorHook with createSelectorHook for improved functionality

In my TypeScript code, I used the following to create a typed selector with Redux: import { useSelector, TypedUseSelectorHook } from 'react-redux'; export interface RootState { user: { account: string; } }; export const useTyped ...

Unable to link to 'amount' because it is not a recognized attribute of 'ng-wrapper'

I recently made some changes to my code and now I'm encountering the error message "Can't bind to 'count' since it isn't a known property of 'ng-container'" Instead of having both the notification component and notificat ...

Modifying the name of a key in ng-multiselect-dropdown

this is the example data I am working with id: 5 isAchievementEnabled: false isTargetFormEnabled: true name: "NFSM - Pulse" odiyaName: "Pulse or" when using ng-multiselect-dropdown, it currently displays the "name" key. However, I want ...

Implementation of a nested interface using a generic and union types

I am seeking to create a custom type that will enable me to specify a property for a react component: type CustomType<T> = { base: T; tablet?: T; desktop?: T; }; export type ResponsiveCustomValue<T> = CustomType<T> | T; This ...

Utilizing Angular to send intricate objects through HTTP POST requests

Service Inquiry public submitBooking(createBooking: CreateBooking) { const body = this.constructRequestBody(createBooking); return this.httpClient.post(this.baseUrl + 'Save', body ) .subscribe(); } private constructReque ...

Tips for bundling TypeScript Angular with internal modules using SystemJS

We have been exploring the idea of transitioning some of our angular projects to typescript, but we are encountering difficulties with internal modules/namespaces. For reference, we have shared a working example on github: https://github.com/hikirsch/Typ ...

What is the proper way to define a new property for an object within an npm package?

Snippet: import * as request from 'superagent'; request .get('https://***.execute-api.eu-west-1.amazonaws.com/dev/') .proxy(this.options.proxy) Error in TypeScript: Property 'proxy' is not found on type 'Super ...