Pairing objects by utilizing a Universal Mapper

Classes Defined:

abstract class ModelBase {
  id: string;
}

class Person extends ModelBase {
  favoriteDog: Dog | undefined;
  favoriteDogId: string | undefined;
  dogs: Dog[]
}

class Dog extends ModelBase {
  id: string;
  ownerId: string;
  name: string;
}

In the scenario of having arrays of Persons and Dogs, I wish to map them using a method like:

const persons = [{ id: 'A', favoriteDog: undefined, favoriteDogId: 'B'}];
const dogs = [{ id: 'B', name: 'Sparky'}];

mapSingle(persons, "favoriteDog", "favoriteDogId", dogs);

console.log(persons[0].favoriteDog?.name); // logs: Sparky

The code snippet for mapping:

  static mapSingle<TEntity extends ModelBase , TDestProperty extends keyof TEntity, TDestPropertyType extends (TEntity[TDestProperty] | undefined)>(
    destinations: TEntity[],
    destinationProperty: keyof TEntity,
    identityProperty: keyof TEntity,
    sources: TDestPropertyType[]) {

    destinations.forEach(dest => {
      const source = sources.find(x => x["id"] == dest[identityProperty]);
      dest[destinationProperty] = source;  // <--- Error Line
    });
  }

Error encountered:

TS2322: Type 'TDestPropertyType | undefined' is not assignable to type 'TEntity[keyof TEntity]'

Type 'undefined' is not assignable to type 'TEntity[keyof TEntity]'.

The error arises due to defining a property as nullable in the method.

Subsequently, a similar method could be created with analogous tactics;

mapMany(persons, 'Dogs', 'OwnerId', dogs);

Additional Resources:

In TypeScript, how to get the keys of an object type whose values are of a given type?

Typescript Playground Example

Answer №1

Initially, let's explore a specific version that highlights a certain issue:

abstract class ModelBase {
  id: string;
}

class Person extends ModelBase {
  favoriteDog: Dog | undefined;
  favoriteDogId: string | undefined;
  dogs: Dog[]
}

class Dog extends ModelBase {
  id: string;
  ownerId: string;
  name: string;
}

function mapSingle(persons: Person[], instanceKey: keyof Person, idKey: keyof Person, dogs: Dog[]) {
  persons.forEach(person => {
    const dog = dogs.find(dog => dog['id'] == person[idKey])
    person[instanceKey] = dog
//  ^^^^^^^^^^^^^^^^^^^
//  (parameter) instanceKey: keyof Person
//    Type 'Dog | undefined' is not assignable to type 'Dog & string & Dog[]'.
//      Type 'undefined' is not assignable to type 'Dog & string & Dog[]'.
//        Type 'undefined' is not assignable to type 'Dog'.(2322)
  })
}

The confusion arises when TypeScript interprets person[instanceKey] as a type of Dog & string & Dog[].

By referring to the keyof documentation, it becomes apparent that there are two possible outcomes: key names and key types.

Interestingly, this seems to exhibit a merging of types (&).

This realization led to the understanding that instanceKey should actually be Dog | undefined, given that we are assigning either Dog or undefined (the result of find) to it.

Delving deeper into a way to exclusively obtain the keys of a specific type, I discovered KeysOfType.

type KeysOfType<T, TProp> = { [P in keyof T]: T[P] extends TProp? P : never}[keyof T];

I proceeded to refactor the types into interfaces:

interface Identifiable {
  id: string;
}

interface DogOwning {
  favoriteDog: Dog | undefined;
  favoriteDogId: string | undefined;
  dogs: Dog[]
}

interface Person extends Identifiable, DogOwning {
}

interface Dog extends Identifiable {
  ownerId: string;
  name: string;
}

This refinement led to the following, which now allows for the assignment of dogs:

function mapSingle(
  owners: Person[], 
  assignedKey: KeysOfType<DogOwning, Dog | undefined>, 
  foreignKey: KeysOfType<DogOwning, string | undefined>, 
  ownable: Dog[]
  ) {
  
  owners.forEach(owner => {
    owner[assignedKey] = ownable.find(entity => entity['id'] == owner[foreignKey])
    
  })
}

Regrettably, it appears challenging to make this generic. When we introduce generics for Person, DogOwning, and Dog, we encounter a similar issue. I am unsure if TypeScript imposes a limitation on the nesting of generics, but it seems to be the case here.

There is a concern that assignedKey and foreignKey are merely validated as properties within the owner object type and of the expected type, without ensuring they are the correct keys. This potential issue might also apply to direct property assignments, posing a potential risk of bugs. Exploring alternative approaches, such as encapsulating this logic in methods on Person or a Coordinator class, could help isolate the code and reduce the likelihood of errors. The alternative solution I proposed moves towards this direction.

Answer №2

Another Perspective

I started to understand that there existed an alternate approach to solving the model problem (potentially). However, I acknowledge that the example may have been simplified and may transcend a mere simplification of the solution.

In simple terms, Person.favouriteDog functions as a getter, rather than necessitating a specific Dog to be copied (by reference) to the Person.

Here is a basic alternative, but it will require further development with utility methods on Person and Dog to ensure referential integrity (or perhaps a Coordinator instance that handles assigning Dogs to Persons)

interface Identifiable {
  id: string
}

interface DogOwning {
  favoriteDog?: Dog
  favoriteDogId?: string
  dogs: Dog[]
}

class Person implements Identifiable, DogOwning {
  constructor(public id: string) {}
  favoriteDogId?: string
  get favoriteDog(): Dog | undefined {
    if (!this.favoriteDogId) {
      return undefined
    }

    const favorite = this.dogs.find(dog => dog.id === this.favoriteDogId)
    if (favorite) {
      return favorite
    }
    throw "Referential Integrity Failure: favoriteDogId must be found in Persons dogs"
  
  }
  public readonly dogs: Dog[] = []
}

class Dog implements Identifiable {
  public ownerId?: string
  constructor(public id: string, public name: string) {}
}

const personA = new Person('A')
console.log(`favoriteDog is ${personA.favoriteDog?.name}`)

const sparky = new Dog('B', 'Sparky')
personA.dogs.push(sparky)
personA.favoriteDogId = sparky.id

console.log(`favoriteDog is ${personA.favoriteDog?.name}`)

personA.dogs.splice(personA.dogs.findIndex(dog => dog.id === sparky.id), 1)

console.log(`favoriteDog is ${personA.favoriteDog?.name}`)

Playground Link

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

What is the best way to individually update elements in an array in Ionic v5?

As a newcomer to Ionic and TypeScript, I would appreciate your kindness in helping me with a problem I am facing. I have an array of objects that get updated when adding an 'exercise', where you can specify the number of sets and reps. The issue ...

Creating a generic class in Typescript that can only accept two specific types is a powerful

When designing a generic class, I am faced with the requirement that a property type must be either a string or number to serve as an index. If attempting something like the code snippet below, the following error will be triggered: TS2536: Type 'T ...

Using ReactJS with Typescript: attempting to interpret a value as a type error is encountered (TS2749)

Currently, I am working on coding a ReactJS class using Typescript and Material-ui in a .tsx file. In one of the custom components that I have created, I need to establish a reference to another component used within this custom component. export class My ...

State in Angular stubbornly refuses to switch despite condition changes

Here is the Typescript code, followed by the HTML: public verifySelection() { let choice = false; if (typeof this.formUser.permissionsTemplateID === undefined) { choice = true; } return choice; } <div class="form-group" ...

What should I do about typescript and ES6?

An error occurred while running my code: [0] app/components/people/details/PersonDetailComponent.ts(27,35): error TS2339: Property 'person' is missing from type '{}'. Here is the code snippet in question: export class PersonDeta ...

What is the best way to define types for an array of objects with interconnected properties?

I need to define a type for an object called root, which holds a nested array of objects called values. Each object in the array has properties named one (of any type) and all (an array of the same type as one). Below is my attempt at creating this type d ...

Incorporate the {{ }} syntax to implement the Function

Can a function, such as toLocaleLowerCase(), be used inside {{ }}? If not, is there an alternative method for achieving this? <div *ngFor="let item of elements| keyvalue :originalOrder" class="row mt-3"> <label class=" ...

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, }: { ...

What causes the cursor in an editable div to automatically move to the front of the div?

<div className="min-w-[600px] min-h-[36.8px]" > <div id={`editableDiv-${Object.keys(item)}-${index}`} className="p-3" contentEditable suppressContentEditableWarning onInput={(e) => onChange(e)} > ...

When transferring the code to the src folder, tRPC encounters issues and stops functioning

Currently, I am working on developing a basic Twitter clone using Next and TRPC. While tRPC is up and running smoothly, I am looking to streamline my code by consolidating it all within the src directory. However, upon moving everything, I encountered an i ...

Adjusting the position of Angular Mat-Badge in Chrome browser

I'm currently using the newest version of Angular and I am attempting to utilize the Angular materials mat-badge feature to show the number of touchdowns a player has thrown. However, in Chrome, the badge position seems to shift when the number is inc ...

Tips for creating basic Jasmine and Karma tests for an Angular application to add an object to an array of objects

I have developed a basic Angular project for managing employee data and I'm looking to test the addProduct function. Can someone guide me on how to write a test case for this scenario? I am not using a service, just a simple push operation. Any assist ...

What are the best practices for preventing risky assignments between Ref<string> and Ref<string | undefined>?

Is there a way in Typescript to prevent assigning a Ref<string> to Ref<string | undefined> when using Vue's ref function to create typed Ref objects? Example When trying to assign undefined to a Ref<string>, an error is expected: co ...

Enhancing Angular functionality with the addition of values to an array in a separate component

I need help with adding a value to an array in the 2nd component when a button in the 1st component is clicked. I am working on Angular 4. How can I achieve this? @Component({ selector: 'app-sibling', template: ` {{message}} <butt ...

how to enhance Request type in Express by adding a custom attribute

In my quest to create a custom middleware function in Node.js using TypeScript, I am facing an issue where I am trying to save a decoded JSON web token into a custom request property called 'user' like so: function auth(req: Request, res: Respo ...

Error in TypeScript due to object being undefined

Exploring TypeScript and facing a challenge with setting properties in an Angular component. When I attempt to define properties on an object, I encounter an error message: ERROR TypeError: Cannot set property 'ooyalaId' of undefined Here is ho ...

The Relationship between Field and Parameter Types in TypeScript

I am currently working on a versatile component that allows for the creation of tables based on column configurations. Each row in the table is represented by a specific data model: export interface Record { attribute1: string, attribute2: { subAt ...

How can I access a file uploaded using dxFileUploader?

I am facing an issue with retrieving a file from dxFileUploader (DevExpress) and not being able to read it in the code behind. The file is only appearing as an object. Here is My FileUploader : { location: "before", ...

Exploring the integration of LeafLet into Next JS 13 for interactive mapping

I'm currently working on integrating a LeafLet map component into my Next JS 13.0.1 project, but I'm facing an issue with the rendering of the map component. Upon the initial loading of the map component, I encountered this error: ReferenceError ...

Utilizing AMD Modules and TypeScript to Load Bootstrap

I am attempting to incorporate Bootstrap into my project using RequireJS alongside typescript AMD modules. Currently, my requireJS configuration looks like this: require.config({ shim: { bootstrap: { deps: ["jquery"] } }, paths: { ...