0

How can I map an Array List from Source to Destination with the two classes below? Need the DTO to map to lookup array. Have been testing map function, still not working. Also, should we make the mapFromSourceAddressDto method to a separate Export function, or inside the Lookup class itself, would it make it easier?

There maybe a chance that the backend DTO API models can change, so trying to create a typesafe way to map subset columns, and be notified if DTO column name changes.

export class SourceOfAddressDto {
    sourceOfAddressId: number | undefined;
    sourceOfAddressCode: string | undefined;
    sourceOfAddressDescription: string | undefined;
    sourceOfAddressLabel: string | undefined;
    createDate: date;
    userId: string;
}

export class SourceOfAddressLookup {
    sourceOfAddressId: number | undefined;
    sourceOfAddressCode: string | undefined;
    sourceOfAddressDescription: string | undefined;
    sourceOfAddressLabel: string | undefined;       
}

export function mapFromSourceOfAddressDto(sourceOfAddressDto: SourceOfAddressDto){
    const sourceOfAddressLookup = new SourceOfAddressLookup();
    sourceOfAddressLookup .sourceOfAddressId = sourceOfAddressDto.sourceOfAddressId
    sourceOfAddressLookup .sourceOfAddressCode = sourceOfAddressDto.sourceOfAddressCode;
    sourceOfAddressLookup .sourceOfAddressDescription = sourceOfAddressDto.sourceOfAddressDescription
    sourceOfAddressLookup .sourceOfAddressLabel = sourceOfAddressDto.sourceOfAddressLabel;

    return sourceOfAddressLookup ;
}

Goal: Take an Array<SourceofAddressDto> ---> Array<SourceOfAddressLookup>

Attempted Solution:

looking for cleaner way,

public addressSourceList: Array<SourceOfAddressLookup>; 

if (addressSourceListDto.length > 0){
    for (let i = 0; i < addressSourceListDto.length; ++i ) {
      this.addressSourceList[i] = mapFromSourceOfAddressDto(addressSourceListDto[i])
    }
  }
ProgrammingLlama
  • 36,677
  • 7
  • 67
  • 86
  • I'm afraid I don't understand the question. Could you edit the code into a [mcve] which clearly shows what you're trying to do and where it's not working? Good luck! – jcalz Feb 20 '20 at 20:34
  • How are you calling the `mapFromSourceOfAddressDto` function? – Heretic Monkey Feb 20 '20 at 20:46
  • see attempted solution above @jcalz –  Feb 20 '20 at 20:58
  • see attempted solution above @HereticMonkey it may help –  Feb 20 '20 at 20:59
  • Is there a particular reason you need classes and not interfaces? Classes should have their properties initialized, so `createDate: date; userId: string;` are errors. And `date` is not a known type; do you mean `Date`? – jcalz Feb 20 '20 at 21:14
  • need methods also later, https://stackoverflow.com/questions/54356711/angular-class-and-interface/54356916 @jcalz –  Feb 20 '20 at 21:17
  • You need methods on `SourceOfAddressDto`, `SourceOfAddressLookup`, or both? – jcalz Feb 20 '20 at 21:19

2 Answers2

1

I think you may be making things more complicated than it needs to be.

A more effective way to ensure type safety in this scenario would be first to define two typescript interfaces.

One for your DTO data structure (which I presume comes from an api request). The other for your 'destination' object structure.

interface SourceOfAddressDto {
  sourceOfAddressId?: number;
  sourceOfAddressCode?: string;
  sourceOfAddressDescription?: string;
  sourceOfAddressLabel?: string;
  createDate: string;
  userId: string;
}

interface SourceOfAddress {
  sourceOfAddressId?: number;
  sourceOfAddressCode?: string;
  sourceOfAddressDescription?: string;
  sourceOfAddressLabel?: string;
}

You can define your map function separately with a return type specified

const mapItems = (item:SourceOfAddressDto):SourceOfAddress[] => {
  return {
    sourceOfAddressId: item.sourceOfAddressId;
    sourceOfAddressCode: item.sourceOfAddressCode;
    sourceOfAddressDescription: item.sourceOfAddressDescription;
    sourceOfAddressLabel: item.sourceOfAddressLabel;
  }
};

When you retrieve your async data you can then map it directly:

const data = await fetch("http://api") as SourceOfAddressDto[];
const mappedData = data.map(mapItems);
Jackson
  • 3,476
  • 1
  • 19
  • 29
  • this is lot nice, however, right now the mapItems returns array, one if I wanted to conduct a 1 on 1 case basis, have reusability in the function? –  Feb 20 '20 at 21:04
  • also, heard I cannot create new instances of interfaces, but classes I can –  Feb 20 '20 at 21:05
  • I don't think you need to create class instances as you are working with very basic object structures. In this case it is fine to use an array of object literals. – Jackson Feb 20 '20 at 21:24
0

NEW ANSWER:

Okay, I'm switching this around: your DTO is an interface corresponding to plain-old-javascript-objects that you get from your API:

interface SourceOfAddressDto {
    sourceOfAddressId?: number | undefined;
    sourceOfAddressCode?: string | undefined;
    sourceOfAddressDescription?: string | undefined;
    sourceOfAddressLabel?: string | undefined;
    createDate: Date;
    userId: string;
}

And your Lookup is a bona fide class with methods that you need to use, like the shout() one I'll make up as an example:

class SourceOfAddressLookup {
    sourceOfAddressId?: number | undefined;
    sourceOfAddressCode?: string | undefined;
    sourceOfAddressDescription?: string | undefined;
    sourceOfAddressLabel?: string | undefined;
    shout() {
        console.log("HELLO MY ID IS " + this.sourceOfAddressId +
            " AND MY CODE IS \"" + this.sourceOfAddressCode + "\"" +
            " AND MY DESCRIPTION IS \"" + this.sourceOfAddressDescription + "\"" +
            " AND MY LABEL IS \"" + this.sourceOfAddressLabel + "\"!");
    }
}

Instead of pluck() from before, I'll define assignProps(), which takes a destination object, a source object, and a list of property keys to copy from the source to the destination. It's generic so the compiler should yell at you if for some reason the source's properties are not the right type for the destination:

function assignProps<T, K extends keyof T>(
    destination: T, 
    source: Pick<T, K>, 
    ...keys: K[]
): T {
    keys.forEach(k => destination[k] = source[k]);
    return destination;
}

So now, mapFromSourceOfAddressDto takes a SourceOfAddressDto and calls assignProps() on a newly constructed SourceOfAddressLookup instance:

const mapFromSourceOfAddressDto = (dto: SourceOfAddressDto) => assignProps(
    new SourceOfAddressLookup(),
    dto,
    "sourceOfAddressId",
    "sourceOfAddressCode",
    "sourceOfAddressDescription",
    "sourceOfAddressLabel"
)

This compiles with no error, so the types should work. Then you can do array mapping pretty easily like this:

class Foo {
    public addressSourceList: Array<SourceOfAddressLookup>;
    constructor(addressSourceListDto: Array<SourceOfAddressDto>) {
        this.addressSourceList = addressSourceListDto.map(mapFromSourceOfAddressDto);
    }
}

And let's test it by constructing something with an array of SourceOfAddressDto objects:

const foo = new Foo([{
    createDate: new Date(),
    userId: "abc",
    sourceOfAddressId: 0,
    sourceOfAddressDescription: "d",
    sourceOfAddressCode: "c",
    sourceOfAddressLabel: "l"
}, {
    createDate: new Date(),
    userId: "def",
    sourceOfAddressId: 1,
    sourceOfAddressDescription: "D",
    sourceOfAddressCode: "C",
    sourceOfAddressLabel: "L"
}]);

That should be mapped by Foo's constructor to an array of SourceOfAddressLookup instances, so let's test it by calling the shout() method for each element:

foo.addressSourceList.forEach(x => x.shout())
// HELLO MY ID IS 0 AND MY CODE IS "c" AND MY DESCRIPTION IS "d" AND MY LABEL IS "l"! 
// HELLO MY ID IS 1 AND MY CODE IS "C" AND MY DESCRIPTION IS "D" AND MY LABEL IS "L"!

Okay, looks good. Good luck again!

Playground link to code


OLD ANSWER:

I assume that your DTO is a full-fledged class with methods

class SourceOfAddressDto {
    sourceOfAddressId: number | undefined;
    sourceOfAddressCode: string | undefined;
    sourceOfAddressDescription: string | undefined;
    sourceOfAddressLabel: string | undefined;
    createDate: Date;
    userId: string;
    constructor(createDate: Date, userId: string) {
        this.createDate = createDate; this.userId = userId;
    }
}

but that the lookup type can be an interface which just has some of the same properties. If you want this type to have a name you can do it this way:

// if you must name this type, you can do this:
interface SourceOfAddressLookup extends Pick<SourceOfAddressDto,
    "sourceOfAddressCode" |
    "sourceOfAddressDescription" |
    "sourceOfAddressId" |
    "sourceOfAddressLabel"
    > { }

Anyway in general I'd use a function like pluck() to take an existing object and make a new one by copying a list of properties:

function pluck<T, K extends keyof T>(t: T, ...k: K[]) {
    return k.reduce((a, k) => (a[k] = t[k], a), {} as Pick<T, K>)
}

And then your mapFromSourceOfAddressDto function can use it like this:

function mapFromSourceOfAddressDto(obj: SourceOfAddressDto): SourceOfAddressLookup {
    return pluck(
        obj,
        "sourceOfAddressId",
        "sourceOfAddressCode",
        "sourceOfAddressDescription",
        "sourceOfAddressLabel"
    );
}

And we can make sure it works:

const dto = new SourceOfAddressDto(new Date(), "abc");
dto.sourceOfAddressCode = "cod";
dto.sourceOfAddressDescription = "descrip";
dto.sourceOfAddressLabel = "lab";
dto.sourceOfAddressId = 1;

const lookup = mapFromSourceOfAddressDto(dto);

console.log(JSON.stringify(lookup));

/* {
"sourceOfAddressId":1,
"sourceOfAddressCode":"cod",
"sourceOfAddressDescription":"descrip",
"sourceOfAddressLabel":"lab"
}
*/

Looks good to me. Hope this helps give you some direction; good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • cool, this is helpful, how do I conduct this use your mapFromSourceAddresss Dto for an array into another array? you did for single item, thanks –  Feb 20 '20 at 21:32
  • `sourceOfAddressDtoArray.map(mapFromSourceOfAddressDto)`? – jcalz Feb 20 '20 at 21:34
  • the DTO comes from API and has no methods, my lookup value has methods it concanetates, capitalizes words, etc –  Feb 20 '20 at 21:34
  • feel free to write in answer, was just trying that, –  Feb 20 '20 at 21:35
  • oh, so I did it backwards... so you need to hydrate a class with props from an object and not the reverse – jcalz Feb 20 '20 at 21:35
  • I have to run now and can't edit anything at the moment; good luck – jcalz Feb 20 '20 at 21:35
  • okay I modified the answer to hopefully more closely reflect what you're doing. – jcalz Feb 21 '20 at 01:48
  • gave points and accepted answer, feel free to thumbs up question, thanks –  Feb 21 '20 at 06:52