Tips for executing a type-secure object mapping in Typescript

I am currently working on creating a function in Typescript that will map an object while ensuring that it retains the same keys. I have attempted different methods, but none seem to work as intended:

function mapObject1<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  return Object.fromEntries(
    Object.entries(object)
      .map(([key, value]): [K, B] => [key, mapper(value)]),
  ); // error!
  // Type '{ [k: string]: B; }' is not assignable to type '{ [P in K]: B; }'
}

export function mapObject2<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  const result: { [P in K]?: B } = {};

  (Object.keys(object) as K[]).forEach((key: K) => {
    result[key] = mapper(object[key]);
  });

  return result; // error!
  // Type '{ [P in K]?: B | undefined; }' is not assignable to type '{ [P in K]: B; }'
}

In mapObject1, when using Object.entries() and Object.fromEntries(), the key types are converted to string. In mapObject2, since result's keys start out empty, they must be optional, leading Typescript to fail recognizing all original keys from object. How can this issue be resolved?

Answer №1

Regrettably, the TypeScript compiler faces challenges in verifying the safety of the implementation due to several reasons. The most practical approach is to ensure that your implementation is correctly written and then utilize type assertions to explicitly inform the compiler about the types of values you claim they possess:

function mapObject1<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  return Object.fromEntries(
    Object.entries(object)
      .map(([key, value]) => [key, mapper(value as A)]),
    // assert --------------------------------> ^^^^^
  ) as { [P in K]: B }
  //^^^^^^^^^^^^^^^^^^ <-- assert
}

function mapObject2<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  const result: { [P in K]?: B } = {};

  (Object.keys(object) as K[]).forEach((key: K) => {
    // assert -------> ^^^^^^ (you already did this)
    result[key] = mapper(object[key]);
  });

  return result as { [P in K]: B };
  // assert --> ^^^^^^^^^^^^^^^^^^
}

Fortunately, everything compiles without any issues.


The compiler faces limitations in comprehending the following logic:

  • The typings for the

    Object.entries() method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries)
    and (the Object.keys() method do not restrict the keys of object to just your generic type K. Since object types in TypeScript are not "sealed," objects may contain more properties than the compiler is aware of. Therefore, the compiler assigns a type of string for keys and unknown for values. Refer to Why doesn't Object.keys return a keyof type in TypeScript? for further insights.

    Hence, some errors arise because the compiler aims to prevent potential pitfalls like this:

    interface Foo { x: number, y: number, z: number }
    const obj = { x: Math.LN2, y: Math.PI, z: Math.E, other: "abc" };
    const foo: Foo = obj; // this assignment is okay
    const oops = mapObject1(foo, num => num.toFixed(2)); 
    /* const oops: { x: string; y: string; z: string; } */
    // 💥 num.toFixed is not a function !!!
    

    While assigning foo of type

    Foo</code allows for additional properties, it leads to runtime errors within the mapping function. Although such circumstances are rare in practice, if you deem them acceptable risks, utilize type assertions and proceed. Otherwise, consider redesigning your function to accept a defined list of keys for transformation. Nonetheless, this falls beyond the scope of the current question.</p>
    </li>
    <li><p>Even if the compiler assumes that <code>Object.keys(object)
    returns all keys, it cannot ascertain that iterating over these keys and populating properties in the return object will elevate the partial object to a complete one. This action is safe but exceeds the compiler's deduction capabilities. See How can i move away from a Partial<T> to T without casting in Typescript for detailed information. In such scenarios, employing a type assertion simplifies the process.

Playground link to code

Answer №2

To overcome the limitations of Typescript, you have the option to define the return type of mapObject when invoking it and then convert the return value to any. While this method may not be completely foolproof, it enables you to maintain type integrity while mapping objects.

function mapObject<R>(
  object: object,
  mapper: (val: any, key: string) => any,
): R {
  return Object.fromEntries(
    Object.entries(object).map(([key, value]) => [key, mapper(value, key)]),
  ) as any;
}

You can utilize this function by specifying the desired parameters, such as excluding a property c from each value in an input object:

const input = {
  k1: {
    b: 1,
    c: 2,
  },
  k2: {
    c: 3,
    d: 4,
  },
} as const;

const mapper = <T extends { c: number }>(value: T) => {
  const { c, ...other } = value;
  return other;
};

const result = mapObject<
  { [k in keyof typeof input]: ReturnType<typeof mapper<typeof input[k]>> }
>(input, mapper);

In the end, typeof result will display:

{
  readonly k1: {
    readonly b: 1;
  };
  readonly k2: {
    readonly d: 4;
  };
}

The underlying reason for implementing this approach lies in the inability to pass a generic parameter to another generic parameter in Typescript, referred to as a higher-kinded type (referenced in this answer).

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

Encountering difficulty in reaching the /login endpoint with TypeScript in Express framework

I'm currently working on a demo project using TypeScript and Express, but I've hit a roadblock that I can't seem to figure out. For this project, I've been following a tutorial series from this blog. However, after completing two parts ...

Having issues with NGXS subscription not functioning properly when selecting a variable

Currently, I am working with Angular 11 and NGXS. One issue I am facing involves a subscription for a variable in the state. Here is the problematic subscription: @Select(state => state.alert.alerts) alerts$: Observable<any[]> ngOnInit(): void { t ...

What steps are involved in launching an outdated Angular project?

Tasked with reviving an old Angular client in my company, I found myself grappling with outdated files and missing configurations. The lack of package.json, package-lock.json, and angular.json added to the confusion, while the presence of node modules in t ...

The recursive component is functional exclusively outside of its own scope

I'm facing an issue where my recursive component is not nesting itself properly. The problem arises when I try to use the Recursive component inside another Recursive component. Although the root is correctly inserted into the Recursive component fro ...

Developing React component libraries with TypeScript compared to Babel compiler

Currently, I'm utilizing the babel compiler for compiling my React component libraries. The initial choice was influenced by Create React App's use of the same compiler. However, I've encountered challenges with using babel for creating libr ...

Detecting the language of a browser

In my Angular2 application, I am looking to identify the browser language and use that information to send a request to the backend REST API with localization settings and variable IDs that require translation. Once the request is processed, I will receive ...

Is there a way to keep a button in a "pressed" state after it has been clicked?

Could someone kindly share the code with me? I am looking for a button that remains pressed after clicking it. At times, I feel embarrassed for struggling with seemingly simple tasks. Grateful for any guidance or solution provided by this community! Man ...

DeActivation only occurs once per route

Having an issue with the CanDeActivate() function in Angular2. The problem arises when a user tries to leave a page while in edit mode, triggering a popup with Yes, No, and Cancel options. If the user clicks on Cancel, the popup closes. If they click on No ...

The error message "Undefined error in Angular 8 when waiting for API call to finish" appears when

if(this.datashare.selectedtableId!=null) { console.log( "inside if condition"); let resp= this.http.get(this.global.apiUrl+"columns/"+this.datashare.selectedtableId); resp.subscribe((data)=>this.users=data); conso ...

What is the best way to perform a query in Angular using Firebase Firestore?

I am attempting to execute queries in Angular 6 using Firebase Firestore. I have this code, and I have already installed the package "npm firebase @angularfire" but it is not working: import { Component } from '@angular/core'; import { A ...

The MemoizedSelector cannot be assigned to a parameter of type 'string'

Currently, my setup involves Angular 6 and NgRX 6. The reducer implementation I have resembles the following - export interface IFlexBenefitTemplateState { original: IFlexBenefitTemplate; changes: IFlexBenefitTemplate; count: number; loading: boo ...

Dealing with router parameters of an indefinite number in Angular 5: A comprehensive guide

Is there a method to efficiently handle an unknown number of router parameters in a recursive manner? For instance: We are dealing with product categories that may have subcategories, which can have their own subcategories and so on. There are a few key ...

Searching within an Angular component's DOM using JQuery is restricted

Want to incorporate JQuery for DOM manipulation within Angular components, but only want it to target the specific markup within each component. Trying to implement Shadow DOM with this component: import { Component, OnInit, ViewEncapsulation } from &apo ...

Challenge faced: Angular array variable not refreshing

I am currently working on a map application where users can input coordinates (latitude and longitude). I want to add a marker to the map when the "Add Waypoint" button is clicked, but nothing happens. Strangely, entering the values manually into the .ts f ...

When the state changes, the dialogue triggers an animation

Currently, I am utilizing Redux along with material-ui in my project. I have been experimenting with running a Dialog featuring <Slide direction="up"/> animation by leveraging the attribute called TransitionComponent. The state value emai ...

Challenges arise when trying to access environment variables using react-native-dotenv in React

I am currently working on two separate projects, one being an app and the other a webapp. The app project is already set up with react-native-dotenv and is functioning as expected. However, when I attempt to use the same code for the webapp, I encounter an ...

Setting up a global CSS and SASS stylesheet for webpack, TypeScript, Phaser, and Angular: A step-by-step guide

A manual configuration has been set up to accommodate all the technologies mentioned in the title (webpack, typescript, phaser, and angular). While it works perfectly for angular component stylesheets, there seems to be an issue with including a global st ...

Inputting data types as arguments into a personalized hook

I am currently developing a Next.js application and have created a custom hook called useAxios. I am looking to implement a type assertion similar to what can be done with useState. For example: const [foo, setFoo] = useState<string>(''); ...

How can I modify the pristine state of NgModel in Angular 2 using code?

Whenever you update the NgModel field, it will automatically set model.pristine to true. Submitting the form does not change the "pristine" status, which is expected behavior and not a bug. In my situation, I need to display validation errors when the fo ...

Can the automatic casting feature of TypeScript be turned off when dealing with fields that have identical names?

Imagine you have a class defined as follows: Class Flower { public readonly color: string; public readonly type: string; constructor(color: string, type: string) { this.color = color; this.type = type; } Now, let's introduce anoth ...