Utilize mapping to object and preserve type inference

I am currently developing a function that utilizes a map function to map objects.

interface Dictionary<T> {
  [key: string]: T;
}

function objectMap<TValue, TResult>(
  obj: Dictionary<TValue>,
  valSelector: (val: TValue) => TResult
) {
  const ret = {} as Dictionary<TResult>;

  for (const key of Object.keys(obj)) {
    ret[key] = valSelector.call(null, obj[key]);
  }
  
  return ret;
}

This function can be used in the following way:

const myObj = {
  withString: {
    api: (id: string) => Promise.resolve(id),
  },
  withNumber: {
    api: (id: number) => Promise.resolve(id),
  },
}

const mapToAPI = objectMap(myObj, (val => val.api));

mapToAPI.withString('some string');

An error occurs on the last line:

Argument of type 'string' is not assignable to parameter of type 'never'.

How do I go about mapping a generic object while preserving type inference?

Answer №1

As pointed out by jcalz, achieving this particular type of arbitrary transformation is not feasible in TS.

However, if you are using the function simply for navigating through the tree structure, there are more efficient methods available.

If your goal is to maintain navigational capabilities within a tree rather than as a flat object, you can leverage tools like lodash and a utility type from type-fest to type the return value.

import _ from "lodash"
import { Get } from "type-fest"

function mapObject<
  Dict extends Record<string, any>,
  Path extends string
>(
  obj: Dict,
  path: Path
): {
    [Key in keyof Dict]: Get<Dict, `${Key}.${Path}`>
} {
  const ret = {} as Record<string, any>

  for (const key of Object.keys(obj)) {
    ret[key] = _.get(obj[key], path)
  }

  return ret as any
}

This approach will work smoothly without requiring explicit typing.

const myObj = {
  withString: {
    api: (id: string) => Promise.resolve(id),
    wrong_api: (id: number) => Promise.resolve(id), //This is equivalent to withString.api
    similar_api: (some_id: string) => Promise.resolve(some_id),
    remove_api: (some_id: boolean) => Promise.resolve(some_id),
    helpers: {
      help: (id: number) => {}
    }
  },
  withNumber: {
    api: (id: number) => Promise.resolve(id),
    helpers: {
      help: (id: number) => {}
    },
    not_an_api: false,
  },
}

const mappedAPIs = objectMap(myObj, 'api');
const mappedHelpers = objectMap(myObj, 'helpers.help');

In addition, for type inferencing on paths to ensure validity, another utility function can be utilized to convert object nodes into a union of strings. However, note that this function is designed for objects and may not function correctly with arrays or large objects.

import { UnionToIntersection } from "type-fest";

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

type IsStrictlyAny<T> = UnionToIntersection<UnionForAny<T>> extends never
  ? true
  : false;

export type ValidPaths<Node, Stack extends string | number = ''> = 
  IsStrictlyAny<Node> extends true ? `${Stack}` :
  Node extends Function ? `${Stack}` :
  Node extends Record<infer Key, infer Item> 
    ? Key extends number | string 
        ? Stack extends ''
          ? `${ValidPaths<Item, Key>}`
          : `${Stack}.${ValidPaths<Item, Key>}`
        : ''
    : ''

Finally, an implementation like this:

function mapObject<
  Dict extends Record<string, any>,
  Path extends ValidPaths<Dict[keyof Dict]>,
>(
  obj: Dict,
  path: Path
): 
{
    [Key in keyof Dict]: Get<Dict, `${Key}.${Path}`>
} {/** Implementation from above **/}

Explore this further on Code Sandbox

Answer №2

This code snippet builds on the insights shared by @kellys and focuses on utilizing the type K explicitly when dealing with other properties within an object:

function objectMap<T, K extends keyof T[keyof T]>(
  obj: T,
  valSelector: (o: T[keyof T]) => T[keyof T][K],
): {
    [P in keyof T]: T[P][K];
} {
  const ret = {} as {
      [P in keyof T]: T[P][K];
  };

  for (const key of Object.keys(obj) as (keyof T)[]) {
    ret[key] = valSelector(obj[key]);
  }
  
  return ret;
}

const myObj = {
  withString: {
    api: (id: string) => Promise.resolve(id),
    bea: "ciao",
  },
  withNumber: {
     api: (id: number) => Promise.resolve(id),
     bea: 123
  },
}

const mapToAPI = objectMap<typeof myObj, "api">(myObj, val => val.api);

mapToAPI.withString('some string');
mapToAPI.withNumber(123)

Playground

Further enhancements to this approach are welcome.

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

TypeScript focuses on checking the type of variables rather than their instance

Is there a way to pass a type (not an instance) as a parameter, with the condition that the type must be an extension of a specific base type? For example abstract class Shape { } class Circle extends Shape { } class Rectangle extends Shape { } class ...

Encountered an issue with ionViewDidLoad: The property 'firstChild' cannot be read as it is null

While working on an Ionic 2 App with Angular2 and Typescript, I encountered an issue when trying to pass a JSON to map for markers. Here is the link to the gist containing the code snippet I am facing an error that reads: view-controller.js:231 MapPage i ...

Upgrading a Basic ReactJS Example to Typescript

Beginner Inquiry I recently converted a ReactJS script from Javascript to Typescript. Is there a more concise way to do this without relying heavily on "any" types? Original Javascript version: const App = ({title}) => ( <div>{title}</div& ...

Encountering a "Duplicate identifier error" when transitioning TypeScript code to JavaScript

I'm currently using VSCode for working with TypeScript, and I've encountered an issue while compiling to JavaScript. The problem arises when the IDE notifies me that certain elements - like classes or variables - are duplicates. This duplication ...

When using my recursive type on Window or Element, I encounter a type instantiation error stating "Type instantiation is excessively deep and possibly infinite.ts"

Recently, I have been using a type to automatically mock interface types in Jest tests. However, upon updating TypeScript and Jest to the latest versions, I encountered an error message stating Type instantiation is excessively deep and possibly infinite.t ...

Is there a way to switch an element across various pages within Ionic 3?

Is it possible to exchange information between two pages using logic? I have successfully implemented a checklist, but I am unsure how to add a success/error icon next to the Room name on the 'verifyplace.html' page after invoking the goToNextPa ...

Material-UI and TypeScript are having trouble finding a compatible overload for this function call

Currently, I'm in the process of converting a JavaScript component that utilizes Material-ui to TypeScript, and I've encountered an issue. Specifically, when rendering a tile-like image where the component prop was overridden along with an additi ...

Show the values in the second dropdown menu according to the selection made in the first dropdown menu using Angular 8

My goal is to retrieve data and populate two dropdowns based on user selection. However, the code I've written isn't giving me the desired output and instead, errors are occurring. Being new to Angular, I would appreciate a review of my code. Her ...

Combine TypeScript files in a specific sequence following compilation

I am hoping to utilize gulp for the following tasks: Compiling TypeScript to JavaScript, which is easily achievable Concatenating JavaScript files in a specific order, proving to be challenging Since I am developing an Angular application, it is crucial ...

Exploring the Material Drawer functionality within an Angular application

I'm having trouble integrating the Material Drawer component into my Angular app. Despite following the instructions on https://material.io/develop/web/components/drawers/, it's not rendering properly. Could someone please provide a detailed, s ...

Angular 2 Component attribute masking

In my Angular 2 component called Foobar, I have defined a property named foobar: import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-foobar', templateUrl: './foobar.component ...

Tips on utilising the datepicker solely with the calendar icon, avoiding the need for any input fields

I'm currently working on a Datatable filter and I would like to incorporate a calendar icon to facilitate date filtering by simply clicking on the Datatable Header. At this stage, I've managed to display a calendar Icon on my Datatable header, b ...

Bringing in TypeScript from external Node packages

I am looking to organize my application by splitting it into separate node modules, with a main module responsible for building all other modules. Additionally, I plan to use TypeScript with ES6 modules. Below is the project structure I have in mind: ma ...

What is the correct way to implement fetch in a React/Redux/TS application?

Currently, I am developing an application using React, Redux, and TypeScript. I have encountered an issue with Promises and TypeScript. Can you assist me in type-defining functions/Promises? An API call returns a list of post IDs like [1, 2, ..., 1000]. I ...

The issue encountered is when the data from the Angular form in the login.component.html file fails to be

I am struggling with a basic login form in my Angular project. Whenever I try to submit the form data to login.components.ts, it appears empty. Here is my login.component.html: <mat-spinner *ngIf="isLoading"></mat-spinner> & ...

Next.js is displaying an error message indicating that the page cannot be properly

Building a Development Environment next.js Typescript Styled-components Steps taken to set up next.js environment yarn create next-app yarn add --dev typescript @types/react @types/node yarn add styled-components yarn add -D @types/styled-c ...

SystemJS could not locate the root directory for RxJS

There seems to be an issue with SystemJS loading rxjs modules on Windows, as it throws a 404 Not Found error on the rxjs directory. This problem does not occur on OSX, and all modules are up to date. GET http://localhost:8080/node_modules/rxjs/ 404 (Not F ...

Could not find the 'injectTapEventPlugin' export in the dependencies of Material-UI related to 'react-tap-event-plugin'

Currently, I am working on a project that involves using react, typescript, material-ui, and webpack. An issue has arisen with importing the injectTapEventPlugin function from the dependency of Material-UI, react-tap-event-plugin. The specific error messag ...

TS2531: Nullability detected in object when using .match() method

I'm encountering a linting error on fileNameMatches[0] in the following code snippet. Strangely, the error doesn't appear on the Boolean() check. Even if I remove that check, the issue remains unresolved. Can anyone suggest a solution? protected ...

Leverage the exported data from Highcharts Editor to create a fresh React chart

I am currently working on implementing the following workflow Create a chart using the Highcharts Editor tool Export the JSON object from the Editor that represents the chart Utilize the exported JSON to render a new chart After creating a chart through ...