Determining the appropriate generic type in Typescript

In my code, there is a method designed to extend an existing key-value map with objects of the same type. This can be useful when working with database query results.

export function extendWith<
  T extends { id: string | number },
  O =
    | (T["id"] extends string | number ? Record<T["id"], T> : never)
    | (T["id"] extends string | number ? Partial<Record<T["id"], T>> : never)
>(obj: O, vals: T | T[]): O {
  const extended = { ...obj };
  const values = Array.isArray(vals) ? vals : [vals];

  for (const val of values) {
    if (val !== undefined) {
      const prop = val["id"];

      if (
        typeof prop === "string" ||
        typeof prop === "number" ||
        typeof prop === "symbol"
      ) {
        (extended as any)[prop] =
          prop in obj ? { ...(obj as any)[prop], ...val } : val;
      }
    }
  }

  return extended;
}

When I use this method and specify the type explicitly as shown below, TypeScript correctly detects errors related to incorrect object properties.

interface Photo {
  id: number;
  name: string;
}
const photos: { [key: number]: Photo } = {
  1: { id: 1, name: "photo-1" },
  2: { id: 2, name: "photo-2" }
};
const extendedPhotos = extendWith<Photo>(photos, { id: 4, name: 3 });

However, when I omit the explicit type declaration in the extendWith call, TypeScript no longer shows these errors. This behavior seems to be related to TypeScript's generic inference system.

If you have any insights on how to ensure correct type inference in this scenario, please share your tips! Your guidance would be greatly appreciated.

Feel free to experiment with a sandbox version of the code here.

Answer №1

Initially, it appears that the first overload in your code is redundant. You are stating that when the id is a string or a number, the returned object will be either a Partial<O> or a full O. Since O will always be valid when mapped to a Partial<O> type, you can simply specify its type as Partial<O>.

In terms of inference, if you allow TypeScript to infer the type, it will use your input to determine the output of the function. It seems like you want to enforce that the second argument of the function MUST be of type Photo, which is not really inference and cannot be inferred unless you pass a variable that is already of type

Photo</code. To enable TS to infer, you would need to replace your final line with something like:</p>
<pre><code>const myPhoto: Photo { id: 4, name: 'my-photo' };
const extendedPhotos = extendWith<Photo>(photos, myPhoto);

This way, TypeScript can utilize the information from the input values to infer the output value.

Answer №2

To achieve this functionality (although this particular example may require further development):

type Identifier = string | number;
type ObjectWithIdentity = { id: Identifier };

function expandWith<O extends Record<Identifier, ObjectWithIdentity>>(object: O, value: O[keyof O]): O {
    (object as Record<Identifier, ObjectWithIdentity>)[value.id] = value;

    return object;
}

The parameters can be simplified to:

  • mandating that object is an object containing values with "id" properties. This requirement can be enforced by only permitting types that extend
    Record<Identifier, ObjectWithIdentity>
    . Any of the following should result in a type error:
expandWith(null, { id: 3, name: "item-4" }); 
expandWith({ abc: 1 }, { id: 3, name: "item-4" });
  • ensuring that value matches the same type as the values within object. This restriction can be achieved using O[keyof O]. Because keyOf represents a union of object's properties, O[keyof O] consists of values from these properties. The subsequent cases should also raise a type error:
interface Item { id: number; name: string; }

const items: Record<number, Item> = {
  1: { id: 1, name: "item-1" },
  2: { id: 2, name: "item-2" }
};

expandWith(items, { id: 4, name: "item-4", abc: 2 });
expandWith(items, { id: 4, name: 1 });
expandWith(items, { name: "abc" });
expandWith(items, null);

Upon invoking expandWith, typescript will infer a more specific type for object than

Record<Identifier, ObjectWithIdentity>
. As a result:

  • This enhanced type can assist in deducing the types of object's values and consequently constraining value.
  • It becomes impossible to append new properties to object since typescript lacks knowledge about whether object remains an extensible Record type (e.g., { a: 1 } is a subtype of Record<string, number>, but additional properties cannot be added). Nonetheless, it is feasible to revert object back to its broader type before extension.

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

Having trouble displaying the image on the screen with Material UI and React while using Higher Order Components (HOC) for

I'm facing an issue with displaying an image on my Material UI Card. Despite checking the directory and ensuring it's correct, the image still doesn't show up. Additionally, I need assistance in modularizing my code to avoid repetition. I at ...

Comparing a series of smaller strings to a larger string for testing purposes

My website has an array filled with bot names. Whenever a user or bot visits the site, I retrieve the user-agent and want to check if any of the values in my array are present in it. var bots = [ "twitterbot", "linkedinbot", "facebookexternalhit", ...

How can I dynamically modify the class name of a specific element generated during an iteration in React when another element is clicked?

My goal is to dynamically add a class to a specific element within a table row when a user clicks a remove button. The desired behavior is to disable the entire row by adding a "removed" class to the containing row div. The challenge is that there are mult ...

Is there a way to access the sqlite3 database file in electron during production?

Currently, I have an electron application that I developed using the create-electron-app package. Inside the public folder of my Electron app, both the main process file and the sqlite3 database are located. During development, I can access the database ...

Displaying Bootstrap alert after a successful jQuery AJAX call

I have been attempting to display an alert on a form once the submission action is completed. Here is my JavaScript function: function submitForm(){ // Initialize Variables With Form Content var nomeCompleto = $("#nomeCompleto").val(); v ...

Error encountered when connecting to MongoDB: 'MongoServerSelectionError'

const uri = 'mongodb+srv:<username>//:<passwod>@chatapp-qrps3.azure.mongodb.net/test?retryWrites=true&w=majority' const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true }) client.c ...

React component making repeated calls to my backend server

As a React newbie, I am currently in the process of learning and exploring the framework. I recently attempted to fetch a new Post using axios. However, upon hitting the '/getPost' URL, I noticed that the GetPost component continuously calls the ...

How to set the element in the render method in Backbone?

Currently, I am in the process of developing a web page utilizing BackboneJS. The structure of the HTML page consists of two divs acting as columns where each item is supposed to be displayed in either the first or second column. However, I am facing an is ...

"Sequencing time with Postgres, manipulating views with Angular

I am encountering issues with displaying graphs in AngularJS. My backend is written in nodeJS and includes an aggregator as a buffer for data received from netdata, which is then inserted into the database using a cron job. Now, with a new client request, ...

The Bootstrap toggler is failing to conceal

Currently, I am working on a website utilizing Bootstrap 5. The issue arises with the navbar - it successfully displays the navigation when in a responsive viewport and the toggler icon is clicked. However, upon clicking the toggler icon again, the navigat ...

What sets Observables (Rx.js) apart from ES2015 generators?

From what I've gathered, there are various techniques used for solving asynchronous programming workflows: Callbacks (CSP) Promises Newer methods include: Rx.js Observables (or mostjs, bacon.js, xstream etc) ES6 generators Async/Await The trend ...

How should I start working on coding these sliders?

Which programming language should I use? My understanding of Java Script is limited, so coding them on my own might be challenging. What would be the essential code to begin with? Here are the sliders - currently just Photoshop images... ...

Toggle the visibility of a div based on whether or not certain fields have been filled

Having a form with a repeater field... <table class="dokan-table"> <tfoot> <tr> <th colspan="3"> <?php $file = [ 'file' => ...

Error: The class constructor [] must be called with the 'new' keyword when creating instances in a Vite project

I encountered an issue in my small Vue 3 project set up with Vite where I received the error message Class constructor XX cannot be invoked without 'new'. After researching online, it seems that this problem typically arises when a transpiled cla ...

Difficulty accessing `evt.target.value` with `RaisedButton` in ReactJS Material UI

My goal is to update a state by submitting a value through a button click. Everything works perfectly when using the HTML input element. However, when I switch to the Material UI RaisedButton, the value isn't passed at all. Can someone help me identif ...

Tips for utilizing MUI Typography properties in version 5

I'm clear on what needs to be done: obtain the type definition for Typography.variant. However, I'm a bit uncertain on how to actually get these. interface TextProps { variant?: string component?: string onClick?: (event: React.MouseEvent&l ...

Is there a way to manipulate text in JQuery without altering the inner element?

I am struggling with an issue in my HTML code. Currently, I have the following structure: <h3 id="price">0.00<sup id="category">N/A</sup></h3> My intention is to use AJAX to replace the content within the <h3 ...

Enhancing Vue.js functionality with extra information stored in local storage

I've created a To Do List app where you can add tasks using a button. Each new task is added to the list with a checkbox and delete button in front of it. I'm trying to save all the values from the inputs and the checked checkboxes on the page on ...

Should using module.export = [] be avoided?

Having two modules that both need access to a shared array can be tricky. One way to handle this is by creating a separate module just for the shared array, like so: sharedArray.js module.exports = []; In your module files, you can then use it like this ...

A Foolproof Method to Dynamically Toggle HTML Checkbox in ASP.NET Using JavaScript

Although there have been numerous inquiries related to this subject, I haven't been able to find a solution specific to my situation. I currently have a gridview that contains checkboxes. What I'm trying to achieve is that when I select the chec ...