typescriptreflection

Fill class properties with key/value data


I have a data class

export interface MusicDto {
    title?: string;
    artist?: string;
    //...
}

I would like to fill it with data from key/value tokens

tokens.forEach(token => {
  const [key, value] = token.split(':');
  if (key && value) {
    const propertyName = key.trim().toLowerCase() as keyof MusicDto;
    const propertyValue = value.replace(';', '').trim();
    let targetProperty = musicData[propertyName];

    if (targetProperty) {
      switch (typeof targetProperty) {
        case 'string': targetProperty = propertyValue;
          break;
        case 'number': targetProperty = parseFloat(propertyValue);
          break;
        default:
      }
    } else {
      console.log(`Unknown property ${propertyName}`);
    }
  }
});

But all properties are going to "Unknown property"

Here a playground which reproduces it


Solution

  • You cannot fill an object this way by assigning to it's extracted value. You need to refer to the property directly. Also your initial class instance is totally empty so you can't get any runtime types from it.

    A solution with fixes for these problems:

    // Here the data
    const anExampleVariable =  `
    #TITLE:ROOM;
    #ARTIST:Blanc Bunny Bandit;
    #BANNER:ROOM.png;
    #BACKGROUND:ROOM-bg.png;
    #CDTITLE:./CDTitles/DDR A20 PLUS.png;
    #MUSIC:ROOM.ogg;
    #SAMPLESTART:57.5;
    #SAMPLELENGTH:15;
    #BPMS:0=145;
    `
    const tokens = anExampleVariable.split('#').map(token => token.trim()).filter(token => token.length > 0);
    
    // Here my target DTO
    class MusicDto {
      title?: string = '';
      artist?: string = '';
      banner?: string = '';
      background?: string = '';
      cdTitle?: string = '';
      music?: string = '';
      sampleStart?: number = 0;
      sampleLength?: number = 0;
      bpms?: string = '';
      notes: number[][][] = [];
    }
    
    const musicData: MusicDto = new MusicDto()
    
    const newData = tokens.reduce((r, item) => {
      const [key, value] = item.split(':');
      r[key.toLowerCase()] = value.slice(0,-1);
      return r;
    }, {} as Record<string, string>);
    
    const foundKeys: string[] = [];
    const keys = <T extends object>(obj: T) => Object.keys(obj) as (keyof T)[];
    keys(musicData).forEach(name => {
      const lower = name.toLowerCase();
      if(lower in newData){
        foundKeys.push(lower);
        const value = newData[lower];
        if(typeof musicData[name] === 'string'){
          (musicData as any)[name] = value; // you could use a custom type guard
        }else if(typeof musicData[name] === 'number'){
          (musicData as any)[name] = parseFloat(value); // you could use a custom type guard
        }
      }
    });
    
    const notFoundKeys = Object.keys(newData).filter(k => !foundKeys.includes(k));
    
    console.log('not found:', notFoundKeys);
    
    console.log(musicData)