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

Compiler error occurs when trying to pass props through a higher-order component via injection

Recently, I have been experimenting with injecting props into a component using a higher order component (HOC). While following this insightful article, I came up with the following HOC: // WithWindowSize.tsx import React, {useEffect, useMemo, useState} fr ...

What sets apart .to and .toService in InversifyJS?

I find the documentation on using .toService(MyClass) for transitive bindings confusing. The examples provided also show that achieving the same result is possible with a regular .to(MyClass). https://github.com/inversify/InversifyJS/blob/master/wiki/tran ...

"Encountered a 'NextAuth expression cannot be called' error

Recently, I delved into learning about authentication in Next.js using next-auth. Following the documentation diligently, I ended up with my app/api/auth/[...nextauth]/route.ts code snippet below: import NextAuth, { type NextAuthOptions } from "next-a ...

Utilizando Typescript com Ionic 2 e AngularJS para autenticar através de um método de post na requisição HTTP e

Greetings and good afternoon to everyone. I hope you all are doing well. I am a beginner in AngularJS, currently using Visual Studio, Ionic 2, and TypeScript. I have successfully connected my app to a REST API in .NET and have implemented a token for tes ...

Having trouble with JavaScript's Date.getUTCMilliSeconds() function?

I have a straightforward question for you. Take a look at this Angular App and try to create a new date, then print the number of UTC milliseconds of that date in the console. Can you figure out why it is returning zero? ...

I'm curious if it's possible to superimpose a png image and specific coordinates onto a map by utilizing react-map

I am attempting to showcase a png graphic on a react-map-gl map, following the approach outlined here. Unfortunately, the image is not appearing as expected and no error messages are being generated for me to troubleshoot. Below is the snippet of code I&a ...

What is the best way to determine the type of `rootReducer`?

My project is set up with a combination of React, Redux, Immutable.js, and TypeScript. As I worked on implementing it, I made an effort to declare types wherever possible which led me to discover an interesting issue. A code example illustrating the proble ...

Using jQuery in Angular, you can add a div element to hidden elements by appending

So, I have a hidden div that I want to show on button click. And not only do I want to show it, but I also want to append another div to it. The show and hide functionality is working fine, but the appending part seems tricky when dealing with hidden eleme ...

Having trouble with Typescript module path resolution for .js files?

I have embarked on a project in React and I am eager to begin transitioning the js files to typescript. The setup for aliases seems to function smoothly when importing .tsx within another .tsx file, however, it encounters issues when attempting to import . ...

Retrieving Information from an Angular 2 Component

Struggling to figure this out, I am attempting to dynamically add user video data that includes a video URL. My goal is to access the data from the component so I can use it in my HTML. I've attempted the following approach. app.component.ts import ...

Even with manual installation, the npm package still encounters dependency errors

Having trouble implementing the Imgur package from NPM into my Angular web app. The installation and import seemed to go smoothly, but when initializing a variable with the package, I encounter compile errors pointing to missing dependencies like 'cry ...

Is Typescript capable of identifying void functions automatically?

Being new to Typescript and programming in general. Instead of using: function greet(greeting: string): void; Can I simplify it like this? Is there any type inference? function greet(greeting: string); ...

Typescript: Delay code execution until a function has completed running

I've encountered an issue with my code that involves calling a function. Here is the snippet of code in question: this.getAllOptions(questionID); console.log("++++++++++++++++++++++++++++++++"); console.log(this.result); The task of this function is ...

In Certain Circumstances, Redirects Are Applicable

I have set up Private Routing in my project. With this configuration, if there is a token stored in the localStorage, users can access private routes. If not, they will be redirected to the /404 page: const token = localStorage.getItem('token'); ...

Is using global variables as a namespace a good practice? Creating ambient TypeScript definitions in StarUML

I'm currently working on creating TypeScript type definitions for the StarUML tool. While I've been successful in defining most of the API, I've hit a roadblock when it comes to linking a JavaScript global variable ("type" in this case) with ...

What are the benefits of utilizing TypeScript declarations? How can you demonstrate their value with concrete examples?

I'm a bit confused about the use of declaration in TypeScript. It seems like the compiler doesn't compile it into the js file, so what is the purpose and advantage of using declaration? Can someone please explain this to me? ...

Exploring the capabilities of the Angular 2 expression parser alongside the functionality of the

Is there a way to create an equivalent of the Angular 1.x ngInit directive in Angular 2? I am familiar with the ngOnInit hook, which is recommended for initialization code. The ngInit directive seems like a quick and declarative way to prototype or fix a ...

I'm looking to configure @types for a third-party React JavaScript module in order to use it with TypeScript and bundle it with webpack. How can I accomplish this?

Imagine you have a third-party npm package called @foo that is all Javascript and has a module named bar. Within your TypeScript .tsx file, you want to use the React component @foo/bar/X. However, when you attempt to import X from '@foo/bar/X', y ...

The Action-Reducer Mapping feature is encountering a type error when handling multiple types of actions

Earlier today, I posed a question about creating a mapping between redux action types and reducers to handle each type explicitly. After receiving helpful guidance on how to create the mapping, I encountered an error when attempting to use it in creating ...

What is the best way to change a blob into a base64 format using Node.js with TypeScript?

When making an internal call to a MicroService in Node.js with TypeScript, I am receiving a blob image as the response. My goal is to convert this blob image into Base64 format so that I can use it to display it within an EJS image tag. I attempted to ach ...