Can Javascript object key name be retrieved using reflection? - javascript

Given the object:
const product = {
food: true,
clothes: false
}
is there a way to programmatically get the name of some key without using Object.keys or similar methods. Something like product.food.getKeyName() which would return a string 'food'. I find that I often have to add object key names to some constants object like:
const products = {
food: 'food',
clothes: 'clothes'
}
which is my primary motivation to figure out a programmatic solution.
Here's an example use case. I want to run over all keys of an object and have different behavior for each key:
Object.keys(product).map(key => {
if (key === 'food') {
// do something specific for food
}
})
but I don't want to write string literals like 'food'.

Thanks to #Enijar's tip indeed it is possible to programmatically retrieve object keys names using Javascript Proxy API as follows:
const product = {
food: true,
clothes: false
}
const proxy = new Proxy(product, {
get: function(originalObject, objectKey) {
if (originalObject.hasOwnProperty(objectKey)) {
return objectKey
}
throw new Error(`The field '${objectKey}' doesn't exist.`)
},
})
console.log(proxy.food) // logs 'food'
console.log(product.food) // logs 'true'

Related

Assign dynamically nested array of classes

I need to be able to receive data from an external API and map it dynamically to classes. When the data is plain object, a simple Object.assign do the job, but when there's nested objects you need to call Object.assign to all nested objects.
The approach which I used was to create a recursive function, but I stumble in this case where there's a nested array of objects.
Classes
class Organization {
id = 'org1';
admin = new User();
users: User[] = [];
}
class User {
id = 'user1';
name = 'name';
account = new Account();
getFullName() {
return `${this.name} surname`;
}
}
class Account {
id = 'account1';
money = 10;
calculate() {
return 10 * 2;
}
}
Function to initialize a class
function create(instance: object, data: any) {
for (const [key, value] of Object.entries(instance)) {
if (Array.isArray(value)) {
for (const element of data[key]) {
// get the type of the element in array dynamically
const newElement = new User();
create(newElement, element)
value.push(newElement);
}
} else if (typeof value === 'object') {
create(value, data[key]);
}
Object.assign(value, data);
}
}
const orgWithError = Object.assign(new Organization(), { admin: { id: 'admin-external' }});
console.log(orgWithError.admin.getFullName()); // orgWithError.admin.getFullName is not a function
const org = new Organization();
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
create(org, data);
// this case works because I manually initialize the user in the create function
// but I need this function to be generic to any class
console.log(org.users[0].getFullName()); // "name surname"
Initially I was trying to first scan the classes and map it and then do the assign, but the problem with the array of object would happen anyway I think.
As far as I understand from your code, what you basically want to do is, given an object, determine, what class it is supposed to represent: Organization, Account or User.
So you need a way to distinguish between different kinds of objects in some way. One option may be to add a type field to the API response, but this will only work if you have access to the API code, which you apparently don't. Another option would be to check if an object has some fields that are unique to the class it represents, like admin for Organization or account for User. But it seems like your API response doesn't always contain all the fields that the class does, so this might also not work.
So why do you need this distinction in the first place? It seems like the only kind of array that your API may send is array of users, so you could just stick to what you have now, anyway there are no other arrays that may show up.
Also a solution that I find more logical is not to depend on Object.assign to just assign all properties somehow by itself, but to do it manually, maybe create a factory function, like I did in the code below. That approach gives you more control, also you can perform some validation in these factory methods, in case you will need it
class Organization {
id = 'org1';
admin = new User();
users: User[] = [];
static fromApiResponse(data: any) {
const org = new Organization()
if(data.id) org.id = data.id
if(data.admin) org.admin = User.fromApiResponse(data.admin)
if(data.users) {
this.users = org.users.map(user => User.fromApiResponse(user))
}
return org
}
}
class User {
id = 'user1';
name = 'name';
account = new Account();
getFullName() {
return `${this.name} surname`;
}
static fromApiResponse(data: any) {
const user = new User()
if(data.id) user.id = data.id
if(data.name) user.name = data.name
if(data.account)
user.account = Account.fromApiResponse(data.account)
return user
}
}
class Account {
id = 'account1';
money = 10;
calculate() {
return 10 * 2;
}
static fromApiResponse(data: any) {
const acc = new Account()
if(data.id) acc.id = data.id
if(data.money) acc.money = data.money
return acc
}
}
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
const organization = Organization.fromApiResponse(data)
I can't conceive of a way to do this generically without any configuration. But I can come up with a way to do this using a configuration object that looks like this:
{
org: { _ctor: Organization, admin: 'usr', users: '[usr]' },
usr: { _ctor: User, account: 'acct' },
acct: { _ctor: Account }
}
and a pointer to the root node, 'org'.
The keys of this object are simple handles for your type/subtypes. Each one is mapped to an object that has a _ctor property pointing to a constructor function, and a collection of other properties that are the names of members of your object and matching properties of your input. Those then are references to other handles. For an array, the handle is [surrounded by square brackets].
Here's an implementation of this idea:
const create = (root, config) => (data, {_ctor, ...keys} = config [root]) =>
Object.assign (new _ctor (), Object .fromEntries (Object .entries (data) .map (
([k, v]) =>
k in keys
? [k, /^\[.*\]$/ .test (keys [k])
? v .map (o => create (keys [k] .slice (1, -1), config) (o))
: create (keys [k], config) (v)
]
: [k, v]
)))
class Organization {
constructor () { this.id = 'org1'; this.admin = new User(); this.users = [] }
}
class User {
constructor () { this.id = 'user1'; this.name = 'name'; this.account = new Account() }
getFullName () { return `${this.name} surname`}
}
class Account {
constructor () { this.id = 'account1'; this.money = 10 }
calculate () { return 10 * 2 }
}
const createOrganization = create ('org', {
org: { _ctor: Organization, admin: 'usr', users: '[usr]' },
usr: { _ctor: User, account: 'acct' },
acct: { _ctor: Account }
})
const orgWithoutError = createOrganization ({ admin: { id: 'admin-external' }});
console .log (orgWithoutError .admin .getFullName ()) // has the right properties
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
const org = createOrganization (data)
console .log (org .users [0] .getFullName ()) // has the right properties
console .log ([
org .constructor .name,
org .admin .constructor.name, // has the correct hierarchy
org .users [0]. account. constructor .name
] .join (', '))
console .log (org) // entire object is correct
.as-console-wrapper {min-height: 100% !important; top: 0}
The main function, create, receives the name of the root node and such a configuration object. It returns a function which takes a plain JS object and hydrates it into your Object structure. Note that it doesn't require you to pre-construct the objects as does your attempt. All the calling of constructors is done internally to the function.
I'm not much of a Typescript user, and I don't have a clue about how to type such a function, or whether TS is even capable of doing so. (I think there's a reasonable chance that it is not.)
There are many ways that this might be expanded, if needed. We might want to allow for property names that vary between your input structure and the object member name, or we might want to allow other collection types besides arrays. If so, we probably would need a somewhat more sophisticated configuration structure, perhaps something like this:
{
org: { _ctor: Organization, admin: {type: 'usr'}, users: {type: Array, itemType: 'usr'} },
usr: { _ctor: User, account: {type: 'acct', renameTo: 'clientAcct'} },
acct: { _ctor: Account }
}
But that's for another day.
It's not clear whether this approach even comes close to meeting your needs, but it was an interesting problem to consider.

How to get the name and props of global Objects in Javascript

I understand that variable names are not intrinsic properties of an object and thus cannot be retrieved when passed through functions. But here is my use-case and I'd like to create a mapping between members of UniversityEnums and displayStrings to get the display string.
const UniversityEnums = {
studentStatus: {Enrolled: 'Enrolled', OnHold: 'OnHold', Expelled: 'Expelled'},
professorStatus: {FullTime: 'FullTime', PartTime: 'PartTime', Emeritus: 'Emeritus', Expelled: 'Expelled'}
};
and
const displayStrings = {
studentStatus_Enrolled: 'Student is enrolled in the program',
studentStatus_OnHold: 'Student decided not to participate',
studentStatus_Expelled: 'Student was expelled',
professorStatus_FullTime: 'Staff member is hired fulltime',
professorStatus_PartTime: 'Staff member is hired parttime',
professorStatus_Emeritus: 'Staff member is retired',
professorStatus_Expelled: 'Staff member was expelled'};
My goal is to write a function that grabs a member of UniversityEnums and returns the corresponding display string, for example:
const expelledStudentDispStr = getDispString(UniversityEnums.studentStatus.Expelled);
console.log(expelledStudentDispStr);
// Student was expelled
The code I currently have has two input arguments and is like const expelledStudentDispStr = getDispString('studentStatus', UniversityEnums.studentStatus.Expelled); which needs the name of the enum to get the value but I am looking for an even smarter way!
Thanks in advance.
Note: that I can manipulate the enum object (for example define it with different variable names (or maybe, maybe, append other properties to it or its children). However, I CANNOT change their values because those values are used to compare those statuses against other variables. Also, the displayStrings is coming from a third party source and modifying them for me is not doable (at least easily!)
Possible workaround:
One way that comes to my mind is to modify my enum object to have names that match displayStrings keys:
const UniversityEnums = {
studentStatus: {
studentStatus_Enrolled: 'Enrolled',
studentStatus_OnHold: 'OnHold',
studentStatus_Expelled: 'Expelled'},
professorStatus: {
professorStatus_FullTime: 'FullTime',
professorStatus_PartTime: 'PartTime',
professorStatus_Emeritus: 'Emeritus',
professorStatus_Expelled: 'Expelled'}
};
Your
getDispString(UniversityEnums.studentStatus.Expelled)
is the same as
getDispString("Expelled")
and it would still be the same with your new object as
getDispString(UniversityEnums.studentStatus.studentStatus_Expelled)
you don't give it more information, on the other hand you could transform your enum like that
const UniversityEnums = {
studentStatus: {
Enrolled: {
status: 'Enrolled',
entity: 'student'
},
OnHold: {
status: 'OnHold',
entity: 'student'
},
Expelled: {
status: 'Expelled',
entity: 'student'
}
}
};
so you would give the extra information you need
you could do something like
for (const kind in UniversityEnums)
for (const value in UniversityEnums[kind])
UniversityEnums[kind][value] = {
kind: kind,
value: value
}
from Bergi's answer
Note that I can manipulate the enum object
In that case, it's easy: just put unique values in the enums so that you can distinguish them properly. For example,
for (const kind in UniversityEnums)
for (const value in UniversityEnums[kind])
UniversityEnums[kind][value] = kind + '_' + value;
With that you can write
function getDispString(enumValue) {
return displayStrings[enumValue];
}

ImmutableJS programatic key naming?

I want to be able to create a Map and programmatically name the index. is
this impossible because Javascript is dynamically typed and Map needs a string?
for more context in case I am missing a better pattern: i get a bunch of person objects (name, id) from server, and now initialize a profile Map.
export function makeMap(person) {
const profileMap = Map({
person.id: Map({
id: person.id,
name: person.name,
foo: false,
otherthing: 5,
favorites: Map({}),
})
})
Not totally sure what you're asking but if you want to use the value of the persons id as a key in an object it would look like:
const { id, name } = person
const profile = {
[id]: {
id,
name,
foo: false,
otherthing: 5,
favorites: {}
}

Create new object from existing object with new values Angular / Ionic 3

I am working on an Ionic 3 project,which uses Angular.
I have a JSON object like below called person. However, I have a Ionic toggle button which enables
various sections based on whats returned from person.
person = { name: "peter", job: "programmer", home: "somewhere"};
person_checked_values = {}
In order to update my toggles I need to pass a boolean. The keys are the same. How can I dynamically build a new object off of
whats returned from person KEYs, but set the value as true so person_checked_values results like below?
person_checked_values = { name: true, job: true, home: true};
I tried to foreach loop person and create a new object from that, but keep getting undefined and stumped. FWIW - I am using _lodash as well so if there is possibly someway to use help from that library its available.
You can use Object.keys to get all of the keys. You can then combine that with the .reduce function of arrays to build an object.
let person = {
name: "peter",
job: "programmer",
home: "somewhere"
};
let result = Object.keys(person).reduce((obj, key) => {
obj[key] = true;
return obj;
}, {})
console.log(result);

Immutable.js Record with (default) unique id

I want to create Immutable.js Records that have a (more or less) unique key. Something like this:
import { Record } from 'immutable'
var MyRecord = Record({
key: Math.random().toString(),
name: ""
})
Is this possible? I've tried it and all records have the same key. I'm importing MyRecord like this:
import { MyRecord } from '../model/MyRecord'
and create new records like this
var r = new MyRecord(data)
where data is a json object.
I could add the key manually after creating the new record of course, but I'd prefer to find a way to automate this.
Thanks for the great question and thanks to #robertklep for leading me to this answer by referring to this: How to construct subclasses of Immutable.Record?
Adapting that answer to work with ids is slightly different: if there is an id given in the record, it won't generate one.
That's the only change really.
// https://gist.github.com/jed/982883
// this can be any function that returns a string
function uuid(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,uuid)}
// this is the implementation you need
class MyRecordWithId extends Immutable.Record({
id: '', // purposely have this empty string
a: 'some a',
b: 'some b'
}) {
constructor(props) {
super(Object.assign({}, props, {id: (props && props.id) || uuid()}))
}
}
// this is a test to see the ID
const myRecord = new MyRecordWithId();
console.log('first record id: ', myRecord.id);
// these are tests to see if we can serialize the record and check if the ID is the same
const json = JSON.stringify(myRecord.toJS());
const js = JSON.parse(json);
const shouldHaveTheSameId = new MyRecordWithId().merge(Immutable.fromJS(js));
console.log('serialized id: ',shouldHaveTheSameId.id);
console.log('are the IDs the same?: ', myRecord.id === shouldHaveTheSameId.id ? 'yes' : 'no');
console.log('different id: ', new MyRecordWithId().id);
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>
You can try this:
function createMyRecord (data){
let record = new MyRecord(data)
return record.set('key', Math.random().toString())
}

Categories