Transfer only designated attributes to object (TS/JS)

Is it feasible to create a custom copy function similar to Object.assign(...) that will only copy specific properties to the target?

The code snippet I have is as follows:

class A {
    foo?: string;
    constructor(p: any) {
        Object.assign(this, p);
    }
}

const instance = new A({
    foo: 'test',
    bar: 'other'
});

console.log(instance); // produces:     A: { "foo": "test", "bar": "other" }
                       // desired output: A: { "foo": "test" }

I understand that types are not preserved in JavaScript, but I am curious if there might be a way to achieve this using decorators or a similar approach.

Using .hasOwnProperty or similar methods won't work because I need to be able to copy unset properties like in the example above.

Answer №1

In TypeScript, there is no built-in support for reflection or introspection. One way to work around this limitation is by adding metadata to your types and creating a custom version of the assign function that utilizes this metadata.

const MAPPED_TYPE_METADATA_KEY = Symbol('mappedType');
type TypeOfType = 'string' | 'number' | 'bigint' | 'boolean' | 'function' | 'object';

/** Decorator that adds mapped types to an object. */
function mappedType(typeName: TypeOfType) {
  return function(target: any, propertyKey: string): void {
    const typeMap = { ...Reflect.get(target, MAPPED_TYPE_METADATA_KEY), [propertyKey]: typeName };
    Reflect.set(target, MAPPED_TYPE_METADATA_KEY, typeMap);
    Reflect.defineMetadata(MAPPED_TYPE_METADATA_KEY, typeMap, target);
  };
}
/** Custom assignment function that uses mapped types to assign values to an object. */ 
function customAssign<T>(obj: T, data: Partial<{ [key in keyof A]: A[key] }>): void {
  const typeMap: Record<string | number | symbol, TypeOfType> | undefined = Reflect.get(obj, MAPPED_TYPE_METADATA_KEY);
  if (typeMap) {
    Object.entries(data)
      .filter(([key, value]) => typeMap[key as keyof T] === typeof value)
      .forEach(([key, value]) => (obj as any)[key] = value);
  }
}
class A {
  @mappedType('string')
  foo?: string;
  @mappedType('number')
  another: number = 1;

  constructor(data: any) {
    customAssign(this, data);
  }
}

const instance = new A({
  foo: 'test',
  bar: 'other'
}); 

console.log(instance); // Output: {another: 1, foo: 'test'}

This solution includes:

  • A mapepdType decorator that creates a type map based on field names and their respective value types. However, it does not enforce type checking.
  • A customAssign function that incorporates the created type map when assigning values to objects.
  • Replacing Object.assign with customAssign in the constructor of class A.

EDIT

I have improved the previous implementation to provide a more type-safe version.

type TypeOfType = 'string' | 'number' | 'bigint' | 'boolean' | 'function' | 'object';
type TypeOfTypeType<T extends TypeOfType> =
  T extends 'string' ? string 
  : T extends 'number' ? number
  : T extends 'bigint' ? bigint
  : T extends 'function' ? Function
  : T extends 'object' ? object
  : never;

function mappedType<T extends TypeOfTypeType<TT>, TT extends TypeOfType>(typeName: TT) {
  return function<U extends object, V extends keyof U>(target: U, propertyKey: U[V] extends (T | undefined) ? V : never): void {
    const typeMap = { ...Reflect.get(target, MAPPED_TYPE_METADATA_KEY), [propertyKey]: typeName };
    Reflect.set(target, MAPPED_TYPE_METADATA_KEY, typeMap);
    Reflect.defineMetadata(MAPPED_TYPE_METADATA_KEY, typeMap, target);
  };
}

This updated approach will trigger a compile-time error if the type specified in the mappedType decorator does not match the actual type of the decorated property. The examples provided demonstrate this concept clearly.

Answer №2

If you desire the resulting object to only retain the properties of the A class, consider implementing the following approach:

class A {
    foo?: string;

    constructor(p: A | any) {
        this.foo = typeof p === 'undefined' || typeof p.foo !== 'string' ? '' : p.foo;
    }
}

For a class with numerous properties:

class A {
    foo?: string;
    bar?: string;
    foo1!: number;
    bar1!: number;

    constructor(p: A | any) {
        Object.getOwnPropertyNames(this).forEach(key => {
            if (typeof p !== 'undefined' && typeof p[key] !== 'undefined') {
                this[key] = p[key];
            }
        });
    }
}

A third option requires explicit default values on the parameters and addresses the error issue "Type 'any' is not assignable to type 'never'.":

class A {
    foo?: string = '';
    bar?: string = '';
    foo1: number = 0;
    bar1: number = 0;

    constructor(p: A | any) {
        let self: A = this;
        let selfKeys = Object.getOwnPropertyNames(self) as Array<keyof A>;
        let pKeys = Object.getOwnPropertyNames(p) as Array<keyof typeof p>;

        selfKeys.filter(k => pKeys.filter((pk) => pk == k).length > 0).forEach((key) => self[key] = (p[key] as never));
    }
}

const i = new A({
    foo: 'test',
    qux: 'not',
});

console.log(i);

As for the fourth option, you can simply remove the optional properties that remain undefined after assignment. This logic has been incorporated within the constructor, but could also be separated into a distinct method:

class A {
    foo?: string = undefined;
    bar?: string = undefined;
    foo1: number = 0;
    bar1: number = 0;

    constructor(p: A | any) {
        let self: A = this;
        let selfKeys = Object.getOwnPropertyNames(self) as Array<keyof A>;
        let pKeys = Object.getOwnPropertyNames(p) as Array<keyof typeof p>;

        selfKeys.filter(k => pKeys.filter((pk) => pk == k).length > 0).forEach((key) => self[key] = (p[key] as never));

        selfKeys.forEach(k => {
            if (self[k] === undefined) {
                delete self[k];
            }
        });
    }
}

const i = new A({
    foo: 'test',
    qux: 'not',
});

console.log(i);

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

Is there a way to access the value of a public variable within the @input decorator of a function type?

I am working on a dropdown component that utilizes the @Input decorator to define a function with arguments, returning a boolean value. dropdown-abstract.component.ts @Input() public itemDisabled: (itemArgs: { dataItem: any; index: number }) => boo ...

Is there a way to retrieve the request URL within the validate function of the http strategy?

Is it possible to access the context object present in guards within the validate method of my bearer strategy, by passing it as an argument along with the token? bearer-auth.guard.ts: @Injectable() export class BearerAuthGuard extends AuthGuard('be ...

Error in TypeScript when using keyof instead of literal in type pattern.Beware of TypeScript error when not

let c = { [X in keyof { "foo" }]: { foo: "bar" } extends { X } ? true : false }["foo"]; let d = { foo: "bar" } extends { "foo" } ? true : false; c and d should both return true, but surprisingly, c is eval ...

Having trouble obtaining search parameters in page.tsx with Next.js 13

Currently, I am in the process of developing a Next.js project with the Next 13 page router. I am facing an issue where I need to access the search parameters from the server component. export default async function Home({ params, searchParams, }: { ...

Is there a workaround in TypeScript to add extra details to a route?

Typically, I include some settings in my route. For instance: .when('Products', { templateUrl: 'App/Products.html', settings: { showbuy: true, showex ...

Unable to combine mui theme with emotion css prop

I recently made the switch from overwriting styles in my styles.css file with !important to using emotion css prop for implementing a dark theme in my web app. Below is the code snippet from App.tsx where I define my theme and utilize ThemeProvider: const ...

The width of mat-table columns remains static even with the presence of an Input field

I'm currently working on an Angular component that serves the dual purpose of displaying data and receiving data. To achieve this, I created a mat-table with input form fields and used {{element.value}} for regular data display. Each column in the tab ...

Double-executing methods in a component

I have encountered an issue while trying to filter existing Worklog objects and summarize the time spent on each one in my PeriodViewTable component. The problem I am facing involves duplicate method calls. To address this, I attempted to reset the value ...

Guide on retrieving a nested JSON array to extract a comprehensive list of values from every parameter within every object

A JSON file with various data points is available: { "success": true, "dataPoints": [{ "count_id": 4, "avg_temperature": 2817, "startTime": "00:00:00", "endTime": "00:19:59.999" }, ... I am trying to extract all the values of & ...

What is the best way to utilize Object.keys() for working with nested objects?

When working with nested objects, I am trying to access the properties using Object.keys() and forEach(). However, I am encountering an issue when attempting to access the nested keys filteringState[item][el]. Is there a specific way to write a function f ...

Tips on transferring key values when inputText changes in ReactJs using TypeScript

I have implemented a switch case for comparing object keys with strings in the following code snippet: import { TextField, Button } from "@material-ui/core"; import React, { Component, ReactNode } from "react"; import classes from "./Contact.module.scss" ...

Exploring the method to deactivate and verify a checkbox by searching within an array of values in my TypeScript file

I am working on a project where I have a select field with checkboxes. My goal is to disable and check the checkboxes based on values from a string array. I am using Angular in my .ts file. this.claimNames = any[]; <div class="row container"> ...

Filtering tables with checkboxes using Next.js and TypeScript

I've recently delved into Typescript and encountered a roadblock. While I successfully tackled the issue in JavaScript, transitioning to Typescript has left me feeling lost. My dilemma revolves around fetching data from an API and populating a table u ...

Performing unit testing on a Vue component that relies on external dependencies

Currently, I am in the process of testing my SiWizard component, which relies on external dependencies from the syncfusion library. The component imports various modules from this library. SiWizard.vue Imports import SiFooter from "@/components/subCompon ...

What is the best way to switch the direction of the arrows based on the sorting order?

Is there a way to dynamically change the direction of arrows based on sorting, similar to the example shown here? sortingPipe.ts: import { SprBitType } from '../spr-bit-type/sprBitType'; import { Pipe, PipeTransform } from '@angular/core& ...

Using Typescript and React to retrieve the type of a variable based on its defined type

Just getting started with Typescript and could use some guidance. Currently, I'm using React to develop a table component with the help of this library: Let's say there's a service that retrieves data: const { data, error, loading, refetc ...

How to display a page outside the router-outlet in angular 4

I'm currently developing an angular 4 application and I am trying to figure out how to load the login.page.ts outside of the router-outlet This is what my home.component.html file looks like: <div class="container"> <top-nav-bar></ ...

Unable to assign a value to the HTMLInputElement's property: The input field can only be set to a filename or an empty string programmatically

When attempting to upload an image, I encountered the error message listed in the question title: This is my template <input type="file" formControlName="avatar" accept=".jpg, .jpeg .svg" #fileInput (change)="uploa ...

Angular 6: A class with no 'default' modifier must explicitly specify a name

I am encountering this error in my ts.file, as I delve into the world of Angular/Ionic. Can anyone help identify the possible reasons for this? I have attempted multiple solutions to address it, but unfortunately, none have proven successful. import { Co ...

Vercel seems to be having trouble detecting TypeScript or the "@types/react" package when deploying a Next.js project

Suddenly, my deployment of Next.js to Vercel has hit a snag after the latest update and is now giving me trouble for not having @types/react AND typescript installed. Seems like you're attempting to utilize TypeScript but are missing essential package ...