Determining the TypeScript function's return type depending on the type of the argument

I am facing a scenario where I have two distinct interfaces: A and B, both sharing a common property. Additionally, I possess two arrays containing items typed with either one of these interfaces. My objective is to create a function that can map these arrays to modify the shared property, while ensuring that the return type matches the type of the argument (A[] or B[]).

To illustrate this challenge, you can view an example in the TS playground link:

interface A {
  requiredField: string;
  optionalField: string;
}

interface B {
  requiredField: string;
}

export const addOptionalData = (
  list: A[] | B[]
) =>
  list.map(listItem => {
    listItem.requiredField = 'new value'
    return listItem;
  })

const list: A[] = [{
  requiredField: 'original value',
  optionalField: 'original value'
}]

let enrichedListA: A[] = addOptionalData(list) // The TS-error occurs at this point
let enrichedListB: B[] = addOptionalData(list)

However, TypeScript throws an error for the enrichedListA variable, stating:

Type '(A | B)[]' is not assignable to type 'A[]'. Type 'A | B' is not assignable to type 'A'. Property 'optionalField' is missing in type 'B', which is required in type 'A'.

In my attempt to address this issue using Generics and conditional types, I encountered new errors in the updated approach as seen in this TS playground link:

interface A {
  requiredField: string;
  optionalField: string;
}

interface B {
  requiredField: string;
}

type ObjectType<T> = 
    T extends A ? A :
    T extends B ? B :
    never;

export const addOptionalData = <T extends A | B>(
  list: T
): ObjectType<T> =>
  list.map(listItem => { // An error arises here stating: "Property 'map' does not exist on type 'A | B'"
    listItem.requiredField = 'new value'
    return listItem;
  })

const list: A[] = [{
  requiredField: 'original value',
  optionalField: 'original value'
}]

let enrichedListA: A[] = addOptionalData(list) // Errors persist here
let enrichedListB: B[] = addOptionalData(list) // And also here

Answer №1

To improve efficiency, consider creating a parent interface Parent and implementing a generic function to handle it. Give this a try:

interface Parent {
  requiredField: string;
}

interface A extends Parent {
  optionalField: string;
}

interface B extends Parent {}

export const addOptionalData = <T extends Parent>(
  list: T[]
) =>
  list.map(listItem => {
    listItem.requiredField = "new value"
    return listItem;
  })

const list: A[] = [{
  requiredField: 'original value',
  optionalField: 'original value'
}]

let enrichedListA: A[] = addOptionalData(list)
let enrichedListB: B[] = addOptionalData(list)

console.log(enrichedListA)
console.log(enrichedListB)

Answer №2

There are several approaches you can take to achieve your goal, depending on the specific data you need to model.

1. Making optionalField an optional field:

This approach is suitable for data that may or may not be present in certain objects. For instance, in a scenario where objects represent cars like A and B, optionalField could indicate the previous owner, which may not be relevant for all cars.

interface A {
  requiredField: string;
  optionalField?: string; // optional
}

// Depending on your use case, you might not even need interface B
interface B {
  requiredField: string;

export const addOptionalData = (
  list: A[] | B[]
) =>
  list.map(listItem => {
    listItem.requiredField = 'new value'
    return listItem;
  })
}

2. Using inheritance to have interface A extend from interface B:

This approach works well if A represents a more detailed version of B</code. For example, if <code>B is a general vehicle and A is a truck with additional properties like bedLength. Here's how you can implement it using generics:


interface A extends B {
  requiredField: string;
  optionalField: string;
}

interface B {
  requiredField: string;
}

export const addOptionalData = <T extends B>(
  list:T[]
) =>
  list.map(listItem => {
    listItem.requiredField = "new value"
    return listItem;
    })

3. Having both interfaces A and B inherit from a common Parent interface:

This approach is useful when both A and B correspond to sub-types of a shared parent type. For example, if A is a truck and B is a car, both falling under the category of Parent vehicles:

interface Parent {
  requiredField: string;
}

interface A extends Parent {
  optionalField: string;
}

interface B extends Parent {}

export const addOptionalData = <T extends Parent>(
  list: T[]
) =>
  list.map(listItem => {
    listItem.requiredField = "new value"
    return listItem;
    })

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

Show blank value if there are no search results, along with an additional menu option

I am currently working on a Typeahead feature with a customized menu using the renderMenu method. In this setup, I have added 2 custom menu items at the bottom - one divider and a button. An issue arises when there are no search results. If I do not inclu ...

How can you ensure a code snippet in JavaScript runs only a single time?

I have a scenario where I need to dynamically save my .env content from the AWS secrets manager, but I only want to do this once when the server starts. What would be the best approach for this situation? My project is utilizing TypeScript: getSecrets(&qu ...

What is the method for extracting user input from a text box on a webpage?

Having trouble with retrieving the value from a text box in my search function. SearchBar Component import { Component, OnInit, Input } from '@angular/core'; @Component({ selector: 'app-search', templateUrl: './search.compon ...

Exploring the process of transmitting error codes from a NestJS controller in your application

Is it possible to send error codes in nestjs other than 200? I attempted to inject the response object into a method, but could not find a way to send an error. save(@Body() body: any, @Res() response: Response): string { console.log("posting...&q ...

Tips for accessing nested values post-subscription in Angular with JSON data?

I have implemented a getReports method that utilizes my web API's get method to retrieve JSON formatted responses. Step1 getReports() { return this._http.get(this.url) .map((response: Response) => response.json()) ...

Using JavaScript or TypeScript to locate the key and add the value to an array

My dilemma involves an object structured as follows: [{ Date: 01/11/2022, Questionnaire: [ {Title: 'Rating', Ans: '5' }, {Title: 'Comment', Ans: 'Awesome' } ] }, { Date: 01/11/2022, Questionnaire ...

Utilizing TypeScript's discriminated unions for function parameters

The function's second argument type is determined by the string value of the first argument. Here is an example of what I am trying to achieve: async action (name: 'create', args: { table: string, object: StorageObject }): Promise<Sto ...

Adding dependency service to the parent class in Angular

I am working with classes parent and child. The child class is an extension of the parent class. I want to inject the injectable class service into the parent class since all instances of the child class will be using it as well. Can someone guide me on ...

Refining a Form Collection in Angular

I have a list of cars that I am converting into a FormArray to display as buttons in an HTML list. My goal is to filter these cars based on their names. CarComponent.html <input #searchBox id="search-box" (input)="search(searchBox.value) ...

When trying to upload a file with ng-upload in Angular, the error 'TypeError: Cannot read properties of undefined (reading 'memes')' is encountered

Struggling with an issue for quite some time now. Attempting to upload an image using ng-upload in angular, successfully saving the file in the database, but encountering a 'Cannot read properties of undefined' error once the upload queue is comp ...

The correct way to type a generic React function component using TypeScript

When attempting to generalize the function component Element into a GenericElement component in TypeScript Playground, I encountered syntax issues that raised complaints from TypeScript. What is the correct method for typing a generic react function compo ...

In TypeScript, it is possible to convert any data type into a numerical map

It appears that typescript permits the casting of any type to a map with keys of type number. Consider this example code: type NumberMap = { [key: number]: string } type AnyType = { foo: string bar: boolean[] } const anyTypeObj: AnyType = { foo: " ...

Beneath the Surface: Exploring Visual Studio with NPM and Typescript

Can you explain how Visual Studio (2015) interacts with external tools such as NPM and the Typescript compiler (tsc.exe)? I imagine that during the building of a solution or project, MSBuild is prompted to execute these additional tools. I'm curious a ...

Create a duplicate of a div element using renderer 2 and angular framework

Good day! I'm currently facing an issue with viewchild, as it's unable to locate my selector in the HTML code. The objective is to find my 'field' selector in the HTML, clone it, and add a new class to it every time a button is clicked. ...

Troubleshooting and setting breakpoints in TypeScript code for Angular Web applications using the Firefox browser

Is there a method to add breakpoints to .typescript source files in my Angular application with Firefox Developer Tools? While I am able to add breakpoints to the generated javascript files, is it possible to debug the .ts source files directly? This quer ...

TreeSelect data mapping lists

Currently tackling an angular project with the goal of converting a list into a specific data structure for compatibility with the TreeSelect component of primeng. The initial data structure is as follows: initialDataStructure = [ { "country&qu ...

Using Typescript to assign a new class instance to an object property

Recently, I crafted a Class that defines the properties of an element: export class ElementProperties { constructor( public value: string, public adminConsentRequired: boolean, public displayString?: string, public desc ...

What is the best way to change between different Angular 2 material tabs using typescript?

I need help with switching tabs using buttons <md-tab-group> <md-tab label="Tab 1">Content 1</md-tab> <md-tab label="Tab 2">Content 2</md-tab> </md-tab-group> <button md-button (click)="showTab1()">Show Tab 1< ...

Getting the true reference of "this" in Typescript

I'm currently working through the tutorial on learn.knockoutjs.com and I've reached step 4 of the custom bindings tutorial. In the provided JavaScript code snippet, we have: update: function(element, valueAccessor) { // Give the first x star ...

Retrieving data from a JSON using Typescript and Angular 2

Here is an example of what my JSON data structure looks like: { "reportSections": [ { "name": "...", "display": true, "nav": false, "reportGroups": { "reports": [ { "name": "...", "ur ...