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

"The TextInput component in ReactNative is preventing me from inputting any text

Experiencing issues with the iOS and Android simulators. Upon typing, the text disappears or flickers. I attempted initializing the state of the texts with preset values instead of leaving them empty. However, this caused the TextInput to stick to the ini ...

Dynamically populate 7 select boxes with options using JQuery

I have a webpage that includes 14 different dropdown menus, one for each day of the week (Monday to Sunday). Each day has two dropdowns: one for opening time and one for closing time. I used jQuery to populate all 14 dropdowns with a pre-defined list of ho ...

Numerous applications of a singular shop within a single webpage

I have a store that uses a fetch function to retrieve graph data from my server using the asyncAction method provided by mobx-utils. The code for the store looks like this: class GraphStore { @observable public loading: boolean; @observable ...

What could be causing my browser to not respond to the JavaScript function for clicking?

I have been struggling to get the images on my browser to change when I click on them. Despite my efforts, I haven't found a solution yet... I've experimented with changing the variables to ".jpg" and also tried removing them altogether. var ...

Combining Vue-Test-Utils with TypeScript typings for wrapper.vm

So, I ran into an interesting situation. Has anyone ever worked with typescript + vue-test-utils and attempted to change a value for testing purposes like this: wrapper.vm.aCoolRefValueToManipulate = 'something much cooler'? I gave it a shot, a ...

AngularJS property sorting: organize your list by name

I have a complicated structure that resembles: { 'street35':[ {'address154': 'name14'}, {'address244': 'name2'} ], 'street2':[ {'address15& ...

Tips for maximizing image efficiency using Next.js and Amazon S3

Currently, I'm utilizing nextjs, react-hook-form, and aws to develop a form incorporating images. Here is my existing setup: form.tsx: <Controller name={'photoDump'} control={control} //{...register('photoDump')} render ...

Using react-hook-form for form submission and managing state in React

I want a button that toggles between displaying "Edit" for entering Edit mode and "Save" for submitting the form. However, the issue is that when I press the button while it shows "Edit," it submits the form. Link to codesandbox with the code provided be ...

Error: Trying to use Router without providing a middleware function. Please make sure to pass a valid middleware function while using Router

While working on my express application with MongoJS, I encountered an issue where despite returning a function, it was showing that an object has been returned instead. To address this, I made sure to include module.exports=router in my JavaScript file. H ...

Animated jQuery carousel with a timer countdown feature

Currently, I am developing a jquery slider/carousel to display various promotions. I am seeking a method to indicate the time left until the next promotion appears. Similar to the flash promo on this website: Do you have any suggestions? ...

Modifying an HTML list item to become 'active' in a navigation bar

One way I've been implementing my navbar on each page is by using the following code at the bottom of the page within script tags: $("#navbar-partial").load("navbar.html). The code for the navbar list looks like this: <ul id="main-nav" class="nav ...

PHP loaded HTML does not allow JavaScript to execute

My system includes an announcements feature where all announcements are retrieved from a database and displayed on the screen using Ajax and PHP. Below is the code snippet used to load each announcement onto the page: echo '<div id="announcements ...

Begin using datatables with the xp:table component

Within an XPage, there is a table component: <xp:table id="tblProposals"> I am looking to apply the datatables plugin to this table using a scriptblock component: <xp:scriptBlock id="scriptInitProposals"> < ...

SCRIPT438: The operation failed because the object does not have the ability to use the 'forEach' property or method

Issue with IE8: Property or method 'forEach' not supported $('.tabs').tabs(); $('#search-consumables [data-ajax-call]').change(function() { var $this = $(this), settings = $this.data(), $target = $(setti ...

Arrange images with haphazard placement

Can someone guide me on creating a block of images when working with an array of random items? ...

Using AngularJS to access form field ids that are generated dynamically

I am dynamically generating form fields using ng-repeat and everything is functioning correctly. However, I now want to incorporate an angular datepicker component that is based on a directive. The issue I am facing is that it only seems to work with stat ...

Calling `$httpBackend.verifyNoOutstandingRequest()` will not result in any errors being raised

During my test setup, I create an HTTP request but do not execute $httpBackend.flush(). Here is the test scenario: describe("Unit: Testing Services", function() { beforeEach(angular.mock.module('EmsWeb.Services')); describe("Unit: DalSer ...

Ways to modify font color in JavaScript "type"

Recently, I came across a fascinating technique where by simply refreshing the page, the text changes sentences. I managed to implement the code successfully, however, I am having trouble changing the color, size, and alignment of the text. <script type ...

Which characters are permissible for the id attribute to prevent the jQuery selector from throwing an exception?

I am facing a situation where the id attribute is inputted by an end user. For instance, if the id for a textbox is "11_=11" as entered by the user, then the HTML code will appear like this: <input type="text" id="11_=11"> The corresponding jQuery ...

Display/Conceal content with JQuery on a PHP webpage

I am having trouble with the following code. My intention is to show/hide the content between the #info id when clicking buttons, but nothing seems to be happening. Could you help me identify the issue? echo '<script> $( "#show' . $r ...