Mapping specific properties in an object using Typescript

I'm diving into TypeScript and aiming to create a straightforward generic function. The function will take an object (potentially partial), a list of keys, and map the specified keys (if they exist) to a different value while keeping the rest of the values unchanged. My goal is to maintain type safety throughout this process.

For example, if we have a type:

interface User {
  userID: string;
  displayName: string;
  email: string;
  photoURL: string;
}

I want to develop a function called mapper that accepts an object of type Partial<User>, along with a list of keys such as

"displayName" | "photoURL"
. For instance, we might want to capitalize the property values for these keys while leaving the other properties untouched.

Here are some sample input/output scenarios:

// INPUT
const fullUser: User = {
    userID: "aaa",
    displayName: "bbb",
    email: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d5b6b6b695b1b1b1fbb6bab8">[email protected]</a>",
    photoURL: "https://eee.fff",
}

const partialUser = {
    userID: "ggg",
    photoURL: "https://hhh.iii",
}

// OUTPUT
const o1 = capsMapper<User, "displayName" | "userID">(fullUser);
const o2 = capsMapper<User, "displayName" | "userID">(partialUser);

// o1 should be
{
    userID: "AAA",
    displayName: "BBB",
    email: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="3b5858587b5f5f5f15585456">[email protected]</a>",
    photoURL: &q;heights;shttps://eee.fff",
}

// o2 should be
{
    userID: "GGG",
    photoURL: "https://hhh.iii",
}

To tackle this requirement, I crafted the following simple generic function:

const mapper = <Type, Keys extends keyof Type>(data: Partial<Type>) => {
  ???
}

I attempted to invoke it using

mapper<User, "displayName" | "photoURL">(myObject)
, but I encountered difficulties in devising the function's inner workings. Specifically, when cycling through the keys of the provided object, I struggled to determine if a key belongs to the Keys type specified generically. I experimented with adding another parameter to the function:

const mapper = <Type, Keys extends keyof Type>(data: Partial<Type>, keys: Keys) => {

or even

const mapper = <Type, Keys extends keyof Type>(data: Partial<Type>, keys: Keys[]) => {

However, attempting to verify the key of the provided (possibly partial) object led to compiler errors.

Is there a way to effectively implement such a function?

Answer №1

function filterObject<T, SelectedProp extends keyof T>(originalObj: Partial<T>, propertyList: Array<keyof T>) {
    const filteredObj:any = {};
    (new Set(propertyList.filter(prop => prop in originalObj))).forEach(prop => filteredObj[prop] = originalObj[prop])
    return filteredObj as Pick<T, SelectedProp>;
}
const obj = {
    name: "John Doe",
    age: 30
};
const result = filterObject<typeof obj, 'age'>(obj, ['age']);
console.log(result.age);

This code snippet is my attempt to achieve the desired outcome. I believe it meets the requirements, although there may be room for improvement. I acknowledge that further enhancements can be made with additional knowledge and time.

I couldn't format this properly as a comment, so pasting here instead.

Answer №2

Passing actual key string arguments into the capsMapper() function is essential. While the TypeScript compiler can handle generic type parameters like K extends keyof T, it's crucial to remember that the static type system, including generics, gets erased when TypeScript is emitted to JavaScript, which ultimately runs at runtime. If there are no values of type K, then these won't be usable at runtime. Therefore, instead of

<T, K extends keyof T>(data: T) => ...
, a more suitable approach would involve something like
<T, K extends keyof T>(data: T, ...keys: K[]) => ...
.

Here is a potential implementation for capsMapper:

const capsMapper = <T extends Partial<Record<K, string>>, K extends keyof T>(
  data: T, ...keys: K[]
) => {
  const d: Omit<T, K> = data;
  const mapped: { [P in K]?: string } = {};
  for (const k of keys) {
    const v = data[k];
    if (typeof v === "string") {
      mapped[k] = v.toUpperCase();
    }
  }

  type WidenString<T> = T extends string ? string : T;
  return { ...d, ...mapped } as { 
    [P in keyof T]: P extends K ? WidenString<T[P]> : T[P] 
  }; 
}

The typing here may seem elaborate, but each part serves its purpose. Only objects of type T with keys of type K</code and <code>string or

undefined</code values in the properties are accepted as input for <code>data
. We widen the type of data from T to Omit<T, K>, essentially disregarding keys in
K</code since they will be overwritten. This processed data is stored in <code>d
for convenience.

Subsequently, we create an object named mapped exclusively for the capitalized properties, whose type mirrors

Partial<Record<K, string>></code. For every <code>k
in the keys array, any identified string properties are capitalized and placed in mapped under the corresponding key.

Finally, by merging d and

mapped</code into a new object and returning it, the output type would appear naturally as <code>Omit<T, K> & Partial<Record<K, string>></code, which is accurate yet not as precise as desired. Because each key in <code>keys</code could potentially be missing in the final output, even if it was present in the original <code>data
.

To ensure specificity, a type assertion is necessary to confirm that the output is indeed of type

{ [P in keyof T]: P extends K ? WidenString<T[P]> : T[P] }
, a mapped type akin to T</code (the type of <code>data) but with any mapped string properties widened to
string</code using the <code>WidenString<T>
conditional type. This distinction is vital when dealing with original
T</code types containing properties with string literal types because converting those to uppercase alters their type.</p>
<p>A test scenario follows below to demonstrate this concept:</p>
<pre><code>// Test scenario

By examining the provided examples, one can observe the correctness of the outputs and the preservation of types—o1 retains the User type, while o2 remains a Partial<User>. Notably, discrepancies arise when handling literal types:

// Example showcasing deviation due to literal types

This instance highlights how the property a within o3 disobeys the requirements set by

Foo</code, indicating why <code>WidenString<T>
is integral. It transforms the input type
{a?: "x" | "y"}
into the output type
{a?: string}</code, appropriately reflecting the altered property value upon uppercasing. To simplify typing in scenarios where such deviations are unexpected:</p>
<pre><code>return { ...d, ...mapped } as T;

This revision acknowledges o3 as Foo despite it failing to align with the prescribed properties. Ultimately, implementing the corrected type assertions ensures greater accuracy in predicting the output types based on the transformation logic applied within capsMapper.

Explore the code in the TypeScript Playground here

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

Change the default parameter value when optional parameters are present

I am working with a function that has four parameters, where the first parameter is mandatory, the second and third are optional, and the fourth has a default value: class MyClass { static myFunc(param1: string, param2? : string, param3? : string, param ...

What is the best way to encode a type that necessitates a specific superclass and interface implementation?

In order to achieve my goal, I need to extend a library class that is part of the React components I am creating. Here's an example of the original library class I'm working with: class ReactComponent<T, U> { render() { return "some ht ...

Angular: No routes found that match the URL segment

I encountered an issue with my routes module where I am receiving the error message Cannot match any routes. URL Segment: 'edit-fighter' when attempting to navigate using the <a> link. The only route that seems to work is the champions-list ...

The limit of update depth when using useState with an array generated from a map operation

I'm working on a component where I'm creating a list from an array using map. Each li element in the list has a ref attached to it to capture the getBoundingClientRect().x and getBoundingClientRect().y coordinates, which are then stored in a refs ...

Error encountered during Svelte/Vite/Pixi.js build: Unable to utilize import statement outside of a module

I'm currently diving into a project that involves Svelte-Kit (my first venture into svelte), Vite, TypeScript, and Pixi. Whenever I attempt to execute vite build, the dreaded error Cannot use import statement outside a module rears its ugly head. Desp ...

Having trouble with a 'Could not find a declaration file for module' error while using my JavaScript npm package?

I recently released a JavaScript npm package that is functioning properly. However, when importing it into another application, there always seems to be three dots in front of the name, along with an error message that reads: Could not find a declaration f ...

Utilize Angular2 to dynamically add new routes based on an array register

Currently, I am utilizing Angular2 for the frontend of my project and I am faced with the task of registering new Routes from an array. Within my application, there is a service that retrieves data from a server. This data is then stored in a variable wit ...

How can I bind form data to an array in Angular?

import { Component, OnInit } from '@angular/core'; import { Senderv3 } from 'app/models/codec/senderv3'; import { CustomerInfoService } from '../../../services/customer-info.service'; import { Router } from '@angular/rout ...

Is it possible to create a typeguard for a complex combinatory type while retaining the existing type information?

Is there a method to create a TypeScript typeguard for a complex combinatory type that includes multiple "and" and "or" statements to check for the presence of one of the "or" types? For example: interface Type1 { cat: string } interface Type2 { d ...

Using TypeScript to execute a function that generates middleware for an Express application

I've developed a middleware for validating incoming data, but I encountered an issue where a function that takes a Joi object as a parameter and returns the middleware is causing errors during build. Interestingly, everything works perfectly fine duri ...

Error with constructor argument in NestJS validator

I've been attempting to implement the nest validator following the example in the 'pipes' document (https://docs.nestjs.com/pipes) under the "Object schema validation" section. I'm specifically working with the Joi example, which is fun ...

Leverage the navigator clipboard feature to save an image in PNG format from a canvas

I've been attempting to save a canvas blob to the navigator clipboard graphicDiv.addEventListener( 'click', (event) => { const canvas = <HTMLCanvasElement> document.getElementById('canvas'); can ...

Why can't Angular iterate through objects using ngFor in Typescript?

Here's what I currently have: public posts: QueryRef<PostsInterface>; this.posts = this._postService.get(); //in ngOnInit In my HTML file, it looks like this: <mat-card *ngFor="let post of posts | async"> This allows me to display eac ...

Error in TypeScript when type-checking a dynamic key within a record

When explicitly checking if a property is not a string using a variable key, Typescript throws an error saying Property 'forEach' does not exist on type 'string'. let params: Record<string, string | string[]>; const key = 'te ...

Using Angular to condense or manipulate an array

I am working with a JSON response that contains arrays of arrays of objects. My goal is to flatten it in an Angular way so I can display it in a Material Table. Specifically, I want to flatten the accessID and desc into a flat array like [ADPRATE, QUOCON, ...

Cause the production build to fail while maintaining successful builds in development mode

I am in the process of developing an Angular application and have a question regarding ensuring the build fails in production but not during local development. Specifically, I have a complex login logic that I would like to bypass while working on it. To ...

What method does Angular use to distinguish between familiar elements and unfamiliar ones?

<bar>- it is a tag with a random name, not officially recognized, so it makes sense that it is considered invalid <div> is valid because it is a standard HTML element - does Angular have a documented list of standard elements? Where can I fin ...

Express-openapi routes are giving a 404 error

As a beginner in the world of openapi, I have been attempting to transition an existing express application to work with openapi. After diligently following the provided documentation, I decided to convert one of my routes to openapi to test it out (disab ...

Creating a PDF file from JSON data and enabling file download in a web application with React and JavaScript

Could you provide guidance on how to convert json data into a pdf file without using any library? Here's an example of the json data I'd like to convert; {"employees":[ {"firstName":"John", "lastName":& ...

It appears that using dedicated objects such as HttpParams or UrlSearchParams do not seem to work when dealing with Angular 5 and HTTP GET query parameters

I'm facing a perplexing issue that I just can’t seem to figure out. Below is the code snippet making a call to a REST endpoint: this.http.get<AllApplicationType[]>(environment.SUDS_API_SERVICE_URL + environment.SUDS_ALL_APPLICATIONS_URL, this ...