Creating a function that can map various types of objects to a single type

type ShapeA = {
  id: number;
  length: string;
  width: string;
  height: string;
}

type ShapeB = {
  id: number;
  side1: string;
  side2: string;
  side3: string;
}

type Shapes = (ShapeA | ShapeB)[]

type MappedShape = {
  id: number;
  dimension1: string;
  dimension2: string;
  dimension3: string;
}

type MapFunction = (shape: Shapes) => MappedShape[];

const mapShapeA: MapFunction = (shape: ShapeA[]) => shape.map(item => ({
  id: item.id,
  dimension1: item.length,
  dimension2: item.width,
  dimension3: item.height
}));

const mapShapeB: MapFunction = (shape: ShapeB[]) => shape.map(item => ({
  id: item.id,
  dimension1: item.side1,
  dimension2: item.side2,
  dimension3: item.side3
}));

While working on the above example, I encountered an issue with TypeScript not recognizing the compatibility between ShapeA and ShapeB in both functions, even though I provided the objects explicitly. It seems like TypeScript should be able to differentiate between the two objects when needed.

Type 'Shapes' is not assignable to type 'ShapeA[]'. Type 'ShapeA | ShapeB' is not assignable to type 'ShapeA'. Type 'ShapeB' is missing properties such as length, width, and height from type 'ShapeA'.

What is a more appropriate TypeScript approach to solving this problem? I believe there may be a misunderstanding on my part regarding unions.

If I were to pass both objects as arguments to the functions, it might work by adding checks for the keys to be returned. However, this could become messy if handling a large number of objects for mapping.

Answer №1

When establishing a MapFn, it is structured as a union of various function types that will be allocated to MapFn instances. However, the purpose of this type might be unclear since the functions designated to it can only manage a specific type of list.

For instance, consider this scenario where ListA, ListB, and MappedList are transformed into ObjectA, ObjectB, and MappedObject respectively, as they are no longer lists. Below is the MapFn type:

type MapFn =
    | ((list: ObjectA[]) => MappedObject[])
    | ((list: ObjectB[]) => MappedObject[]);

(Remember to include () around each function type for TypeScript interpretation.)

Collectively, we have:

type ObjectA = {
    id: number;
    x: string;
    y: string;
    z: string;
};

type ObjectB = {
    id: number;
    a: string;
    b: string;
    c: string;
};

type MappedObject = {
    id: number;
    a: string;
    s: string;
    d: string;
};

type MapFn =
    | ((list: ObjectA[]) => MappedObject[])
    | ((list: ObjectB[]) => MappedObject[]);

const mapListA: MapFn = (list: ObjectA[]) =>
    list.map((el) => ({
        id: el.id,
        a: el.x,
        s: el.y,
        d: el.z,
    }));

const mapListB: MapFn = (list: ObjectB[]) =>
    list.map((el) => ({
        id: el.id,
        a: el.a,
        s: el.b,
        d: el.c,
    }));

Playground link

Answer №2

Do you think these two types are completely different? I have a feeling they might have some similarities since you're aiming to map them into a common type. If that's the case, a discriminator could be useful.

For instance, a Discount type could be either a percent discount or an amount discount. An AmountDiscount would only have an amount property and no percent, while a PercentDiscount may have a percent property but no amount.

interface AmountDiscount {
  id: string;
  amount: 10
}

interface PercentDiscount {
  id: string;
  percent: 5
}

The ideal scenario is to be able to handle all objects of type

Discount</code, regardless of whether they are <code>Percent
or Amount, in a type-safe manner. By type-safe, I mean you wouldn't want a Discount type that has both percent and amount properties, and one of them is sometimes null. This could lead to confusion for the user of the type, as they would need to determine which property is populated and could potentially use percent or amount incorrectly in certain situations.

One solution is to use the in keyword to check if a specific property exists on an object.

Link to example demonstrating the use of the in keyword

This is how I would approach it personally: check out this sandbox. Basically, you would have an enum that explicitly defines the different types you want to map to a common type. There would be a base type that includes this enum and any other shared properties among the distinct types you are mapping. Then, for each type you want to map, extend the base class, specify the particular type it is, and define any type-specific properties. Finally, combine all these types into a single union type, and use a switch or if statements to determine the type and map them to the common type. Here's an example of using a discriminator property, establishing a base type, extending it for specific types, and processing it using a union type. You should be able to derive what you need to do from that. Feel free to ask more questions.

CodeSandbox with an example

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

elimination of nonexistent object

How can I prevent releasing data if two attributes are empty? const fork = [ { from: 'client', msg: null, for: null }, { from: 'client', msg: '2222222222222', for: null }, { from: 'server', msg: 'wqqqqqqqq ...

TypeScript and Redux mapDispatchToProps are not in sync

Below is my React component written in TypeScript: import React from 'react'; import {connect, ConnectedProps} from 'react-redux'; import logo from './assets/logo.png'; // import { Counter } from './features/counter/Count ...

The requested property does not exist within the 'CommonStore' type in mobx-react library

I'm diving into the world of TypeScript and I'm running into an issue when trying to call a function from another class. I keep getting this error - could it be that functions can only be accessed through @inject rather than import? What am I mis ...

Adding a total property at the row level in JavaScript

Here is a JavaScript array that I need help with: [{ Year:2000, Jan:1, Feb: }, {Year:2001, Jan:-1, Feb:0.34 }] I want to calculate the total of Jan and Feb for each entry in the existing array and add it as a new property. For example: [{ Year:2000, Ja ...

What is the best method for accessing a store in Next.js with Redux Toolkit?

Currently, I am working on incorporating integration testing for my application using Jest. To achieve this, I need to render components in order to interact with various queries. However, in order to render a component, it must be wrapped in a Provider to ...

Communicating Progress Updates from C# to Angular 6 Using HttpPost

I'm building an Angular 6 application with a progress bar that displays the rendering and downloading progress of a PDF file as a percentage. Here's my Post call: renderReport(renderObjectId: number): Observable<HttpEvent<Blob>> { ...

Tips for connecting data to an HTML page with Angular 2

My code is throwing an error message while I'm debugging: Error: Uncaught (in promise): TypeError: Unable to get property 'standard' of undefined or null reference Here is the relevant part of my code: student.component.ts: this._studentSe ...

Utilizing the dialogue feature within Angular 6

Situation: I am managing two sets of data in JSON format named customers and workers: customers: [ { "cusId": "01", "customerName": "Customer One", "email": "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data- ...

Creating an HTTP method handler function in Next.js API routes with an unspecified number of generic parameters

Looking to create a wrapper function in NextJS for API routes that can handle multiple HTTP methods with different handlers. For example, check out the TS playground interface GetResponse { hello: string, } // empty object type PostResponse = Record&l ...

Is there a way to dynamically alter the fill color of an SVG component using props along with tailwindcss styling?

Having a bit of trouble cracking this code puzzle. I've got a logo inside a component, but can't seem to pass the className fill options correctly. This is all happening in a NextJS environment with NextUI and tailwind css. const UserLogo = (prop ...

Understanding the Use of Promises and Async/Await in Typescript

Struggling with asynchronous libraries in Typescript, I find myself looking for a way to wait for promises to be resolved without turning every method into an async function. Rather than transforming my entire object model into a chain of Promises and asyn ...

TypeScript maintains the reference and preserves the equality of two objects

Retrieve the last element of an array, make changes to the object that received the value, but inadvertently modify the original last position as well, resulting in both objects being identical. const lunchVisit = plannedVisits[plannedVisits.length ...

Angular2 displays an error stating that the function start.endsWith is not recognized as a valid function

After switching my base URL from / to window.document.location, I encountered the following error message: TypeError: start.endsWith is not a function Has anyone else experienced this issue with [email protected]? ...

"Looking to log in with NextAuth's Google Login but can't find the Client Secret

I am attempting to create a login feature using Next Auth. All the necessary access data has been provided in a .env.local file. Below are the details: GOOGLE_CLIENT_ID=[this information should remain private].apps.googleusercontent.com GOOGLE_CLIENT_SECR ...

Is it possible to switch the hamburger menu button to an X icon upon clicking in Vue 3 with the help of PrimeVue?

When the Menubar component is used, the hamburger menu automatically appears when resizing the browser window. However, I want to change the icon from pi-bars to pi-times when that button is clicked. Is there a way to achieve this? I am uncertain of how t ...

What is the easiest way to compile a single .ts file in my src folder? I can achieve this by configuring the tsconfig.js file and running the yarn

{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, ...

"When attempting to render a Node inside the render() method in React, the error message 'Objects are not valid as a React child' is

On my webpage, I have managed to display the following: export class OverworldComponent extends React.Component<OverworldComponentProps, {}> { render() { return <b>Hello, world!</b> } } However, instead of showing Hello, ...

How can you display a set of components in React using TypeScript?

I'm currently working on rendering an array of JSX Components. I've identified two possible methods to achieve this. Method one (current approach): Create the array as an object type that stores the component properties and then build the JSX co ...

Mastering the Art of Injecting Objects from the Server

Utilizing Angular Universal, I am serving my Angular application through an Express server. The objective is to embed an environment object (from the server) into my application. To achieve this, I have created an InjectionToken export const ENVIRONMENT ...

Setting up TypeScript to function with Webpack's resolve.modules

Imagine having a webpack configuration that looks like this: resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], modules: ['my_modules', 'node_modules'], }, You have a ...