How can I reduce this if statement in JavaScript
if(obj.attributes && obj.attributes.email === 'test#test.com') { ... }
The line by it self is clear, however if you are looking a way to write less && operator inside you can always put things outside of the comparison such as.
var attributes = obj.attributes || {};
if ( attributes.email === 'test#test.com' ) {
}
This makes sense if you need to make multiple checks instead of a single one, however if is a single comparison it seems like the code you already have is okay as you are making sure attributes is defined before accessing an undefined property.
On the other hand if you have support for ES 2015 you can destruct stuff like:
const { attributes = {} } = obj;
if ( attributes.email === 'test#test.com' ) {
}
You can create a reusable get function using Array.reduce(). The function parameters are a path, the object, and a defaultValue (default defaultValue is undefined). It will iterate the path, and try to extract the value, if it fails, it will return the defaultValue:
const get = (path, obj, defaultValue) => obj ?
path.reduce((r, k) => r && typeof r === 'object' ? r[k] : defaultValue, obj)
:
defaultValue;
if(get(['attributes', 'email'], null) === 'test#test.com') { console.log(1) }
if(get(['attributes', 'email'], {}) === 'test#test.com') { console.log(2) }
if(get(['attributes', 'email'], { attributes: {} }) === 'test#test.com') { console.log(3) }
if(get(['attributes', 'email'], { attributes: { email: 'test#test.com' } }) === 'test#test.com') { console.log(4) }
There is a TC39 stage proposal called "Optional Chaining for JavaScript". If it will make it's way to the language, it will add an the optional chaining operator - ?. Now if attributes don't exist, it will return undefined.
Example: obj.attributes?.email
It's usable today via babel plugin.
Related
Currently, I am using the toJSON() object on a method to ignore any fields that are underscored e.g.
toJSON() {
const properties = Object.getOwnPropertyNames(this);
const publicProperties = properties.filter(property => {
return property.charAt(0) !== '_'
})
const json = publicProperties.reduce((obj, key) => {
obj[key] = this[key]
return obj
}, {})
return json
}
This was fine. But I have the concept of roles in my API and I would like to return private fields if the user is an admin.
This led me to the idea of doing:
toJSON(role='user') {
const properties = Object.getOwnPropertyNames(this);
const publicProperties = properties.filter(property => {
return property.charAt(0) !== '_' || role === 'admin'
})
const json = publicProperties.reduce((obj, key) => {
key = key.charAt(0) === '_' ? key.substring(1) : key
obj[key] = this[key]
return obj
}, {})
return json
}
But then the issue becomes how do I get the role argument passed to the toJSON() method, especially when JSON.stringify() is being called and the method calling JSON.stringify() I might not have access to.
I could set on my object a role property before returning a json response e.g.
const getCurrentProject = async (c) => {
const project = await projectService.getCurrentProject(c.get('projectId'));
project._role = c.get('payload').role
return c.json(project, httpStatus.OK);
};
But that doesn't seem ideal and then there are more issues when JSON.stringify() is called on an array of object as I would have to set that for each object.
My next idea was to use my own json response function that would have a replacer function for JSON.stringify()
const jsonResponse = (context, object, status) => {
const role = c.get('payload').role
const body = JSON.stringify(object, (key, value) => {
// function to set private vars to null based on role
})
headers = 'application/json; charset=UTF-8'
return c.body(body, status, headers)
}
The issue with this is that the replacer function will just set them to null and not hide them and I can't just blindly remove keys with null values as I might need them. I could set them to 'remove' or another placeholder and remove them after but again, it doesn't seem like the best way.
So currently I am confused on what I should do. Is there a way to globally override JSON.stringify() and add the role parameter as an argument, is there a better approach I am missing? Or should I just stick to the _role property and for lists of objects set it for each one.
Thanks!
You can use a replacer function. If you return a Function, Symbol, or undefined, the property is not included in the output. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter
I would use Object.entries, and then Array.filter the keys you want, finally Object.fromEntries to get back to an object.
example ->
const obj = {
_adminOnly: 'Lets have a party!',
name: 'bob',
age: 22,
_hideme: 'Hide unless admin',
toJSON: function (role='user') {
return Object.fromEntries(
Object.entries(this).
filter(([k]) => {
if (k === 'toJSON') return false;
return role === 'admin'
? true
: k.charAt(0) !== '_'
}
)
);
}
}
console.log(obj.toJSON());
console.log(obj.toJSON('admin'));
What is a better way of doing this. I'am assigning either of two property values (from two different objects), depending on their existence, to a third data-structure.
In case the args object's value is nullish a non nullish value gets accessed from the default object and assigned to the final structure.
return {
first: {
visible: args.first?.visible ?? defaulttest.first?.visible,
emoji: args.first?.emoji ?? defaulttest.first?.emoji,
style: args.first?.style ?? defaulttest.first?.style,
},
back: {
visible: args.back?.visible ?? defaulttest.back?.visible,
emoji: args.back?.emoji ?? defaulttest.back?.emoji,
style: args.back?.style ?? defaulttest.back?.style,
},
page: {
visible: args.page?.visible ?? defaulttest.page?.visible,
emoji: args.page?.emoji ?? defaulttest.page?.emoji,
style: args.page?.style ?? defaulttest.page?.style,
},
forward: {
visible: args.forward?.visible ?? defaulttest.forward?.visible,
emoji: args.forward?.emoji ?? defaulttest.forward?.emoji,
style: args.forward?.style ?? defaulttest.forward?.style,
},
last: {
visible: args.last?.visible ?? defaulttest.last?.visible,
emoji: args.last?.emoji ?? defaulttest.last?.emoji,
style: args.last?.style ?? defaulttest.last?.style,
},
Mdelete: {
visible: args.Mdelete?.visible ?? defaulttest.Mdelete?.visible,
emoji: args.Mdelete?.emoji ?? defaulttest.Mdelete?.emoji,
style: args.Mdelete?.style ?? defaulttest.Mdelete?.style,
},
removeBtn: {
visible: args.removeBtn?.visible ?? defaulttest.removeBtn?.visible,
emoji: args.removeBtn?.emoji ?? defaulttest.removeBtn?.emoji,
style: args.removeBtn?.style ?? defaulttest.removeBtn?.style,
},
};
From my above comments ...
1/2 ... The OP actually is not really comparing. For a certain set of properties the OP looks up each property at a target object, and only in case it features a nullish value there will be an assignment from a source object's counterpart to the missing property. Thus an approach I would choose was ...
2/2 ... implementing a generic function which merges two objects in a way that a source property can only be written/assigned in case the target structure does not already provide a non nullish value. This function then has to be invoked twice once for args and defaulttest and a second time for the to be returned entirely empty data structure and args.
The above statement was a bit ambitious for there are at least 2 strategies of how one could achieve such kind of mergers.
Thus the below provided example code implements two approaches
one, called refit, which follows a pushing/patching agenda due to forcing the assignement of every non nullish property in a source-object to its non nullish counterpart of a target-object.
a 2nd one, called revive, which resembles a pulling approach for it just reassigns the nullish target-object properties with their non nullish source-object counterparts.
The difference in the results they produce for one and the same preset is going to be demonstrated herby ...
// "refit" ... a pushing/patching approach.
// - force the assignement of every non nullish property in source
// to its non nullish counterpart in target ... hence a *refit*.
function refitNullishValuesRecursively(target, source) {
if (
// are both values array-types?
Array.isArray(source) &&
Array.isArray(target)
) {
source
// for patching always iterate the source items ...
.forEach((sourceItem, idx) => {
// ... and look whether a target counterpart exists.
if (target[idx] == null) {
// either assign an existing structured clone ...
if (sourceItem != null) {
target[idx] = cloneDataStructure(sourceItem);
}
} else {
// ... or proceed recursively.
refitNullishValuesRecursively(target[idx], sourceItem);
}
});
} else if (
// are both values object-types?
source && target &&
'object' === typeof source &&
'object' === typeof target
) {
Object
// for patching ...
.entries(source)
// ... always iterate the source entries (key value pairs) ...
.forEach(([key, sourceValue], idx) => {
// ... and look whether a target counterpart exists.
if (target[key] == null) {
// either assign an existing structured clone ...
if (sourceValue != null) {
target[key] = cloneDataStructure(sourceValue);
}
} else {
// ... or proceed recursively.
refitNullishValuesRecursively(target[key], sourceValue);
}
});
}
return target;
}
// "revive" ... a pulling approach.
// - just reassign the nullish target properties with their
// non nullish source counterparts ... hence a *revive*.
function reviveNullishValuesRecursively(target, source) {
if (
// are both values array-types?
Array.isArray(target) &&
Array.isArray(source)
) {
target
// for fixing always iterate the target items.
.forEach((targetItem, idx) => {
if (targetItem == null) {
// either assign an existing structured clone ...
target[idx] = cloneDataStructure(source[idx]) ?? targetItem;
} else {
// ... or proceed recursively.
reviveNullishValuesRecursively(targetItem, source[idx]);
}
});
} else if (
// are both values object-types?
target && source &&
'object' === typeof target &&
'object' === typeof source
) {
Object
// for fixing ...
.entries(target)
// ... always iterate the target entries (key value pairs).
.forEach(([key, targetValue], idx) => {
if (targetValue == null) {
// either assign an existing structured clone ...
target[key] = cloneDataStructure(source[key]) ?? targetValue;
} else {
// ... or proceed recursively.
reviveNullishValuesRecursively(targetValue, source[key]);
}
});
}
return target;
}
const cloneDataStructure =
('function' === typeof structuredClone)
&& structuredClone
|| (value => JSON.parse(JSON.stringify(value)));
const targetBlueprint = {
x: { xFoo: 'foo', xBar: 'bar', xBaz: { xBiz: null } },
y: { yFoo: 'foo', yBar: null },
};
const patch = {
x: { xFoo: null, xBar: null, xBaz: { xBiz: 'biz' } },
y: { yFoo: null, yBar: 'bar', yBaz: { yBiz: 'biz' } },
};
let target = cloneDataStructure(targetBlueprint);
console.log('"refit" ... a pushing/patching approach.');
console.log('before refit ...', { target, patch });
refitNullishValuesRecursively(target, patch);
console.log('after refit ...', { target, patch });
target = cloneDataStructure(targetBlueprint);
console.log('"revive" ... a pulling approach.');
console.log('before revive ...', { target, patch });
reviveNullishValuesRecursively(target, patch);
console.log('after revive ...', { target, patch });
.as-console-wrapper { min-height: 100%!important; top: 0; }
As for the OP's example which targets the creation of kind of a config-object, one could fully patch/refit a clone of the temporary or current args-config, whereas within the last step one has control over the config-object's final structure by providing the most basic empty config-base which just gets revived by the before created full patch/refit-config.
function refitNullishValuesRecursively(target, source) {
if (Array.isArray(source) && Array.isArray(target)) {
source
.forEach((sourceItem, idx) => {
if (target[idx] == null) {
if (sourceItem != null) {
target[idx] = cloneDataStructure(sourceItem);
}
} else {
refitNullishValuesRecursively(target[idx], sourceItem);
}
});
} else if (
source && target &&
'object' === typeof source &&
'object' === typeof target
) {
Object
.entries(source)
.forEach(([key, sourceValue], idx) => {
if (target[key] == null) {
if (sourceValue != null) {
target[key] = cloneDataStructure(sourceValue);
}
} else {
refitNullishValuesRecursively(target[key], sourceValue);
}
});
}
return target;
}
function reviveNullishValuesRecursively(target, source) {
if (Array.isArray(target) && Array.isArray(source)) {
target
.forEach((targetItem, idx) => {
if (targetItem == null) {
target[idx] = cloneDataStructure(source[idx]) ?? targetItem;
} else {
reviveNullishValuesRecursively(targetItem, source[idx]);
}
});
} else if (
target && source &&
'object' === typeof target &&
'object' === typeof source
) {
Object
.entries(target)
.forEach(([key, targetValue], idx) => {
if (targetValue == null) {
target[key] = cloneDataStructure(source[key]) ?? targetValue;
} else {
reviveNullishValuesRecursively(targetValue, source[key]);
}
});
}
return target;
}
const cloneDataStructure =
('function' === typeof structuredClone)
&& structuredClone
|| (value => JSON.parse(JSON.stringify(value)));
const defaultConfig = {
first: {
visible: 'default.first.visible',
emoji: 'default.first.emoji',
style: 'default.first.style',
},
forward: {
visible: 'default.forward.visible',
emoji: 'default.forward.emoji',
style: 'default.forward.style',
},
removeBtn: {
visible: 'default.removeBtn.visible',
emoji: 'default.removeBtn.emoji',
style: 'default.removeBtn.style',
},
};
const currentConfig = {
first: {
visible: 'current.first.visible',
emoji: 'current.first.emoji',
style: 'current.first.style',
},
forward: {
visible: 'current.forward.visible',
emoji: null,
},
FOO: {
visible: 'current.FOO.visible',
emoji: 'current.FOO.emoji',
style: 'current.FOO.style',
}
};
function getConfiguration(baseConfig) {
return reviveNullishValuesRecursively(
cloneDataStructure(baseConfig),
refitNullishValuesRecursively(
cloneDataStructure(currentConfig),
defaultConfig,
),
);
}
console.log(
getConfiguration({
first: null,
forward: null,
removeBtn: null,
})
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
If the structure of your object is the one you presented you can do:
function normalize(input, defaultValue) {
// Loop on the outer keys
Object.keys(input).forEach(mainKey => {
// Loop on the inner keys
Object.keys(input[mainKey]).forEach(key => {
// set the value of the key as itself or default if null
input[mainKey][key] = input[mainKey]?.[key] ?? defaultValue[mainKey]?.[key]
})
})
return input;
}
Calling normalize(args, defaulttest) you will loop on each inner key, check if it exist and if it does not exist you substitute it with the default in the same path.
Example:
const x = {
a: {a1: '1', a2: '2'},
b: {b1: '1', b2: null}
}
const y = {b: {b2: '5'}}
console.log(normalize(x,y))
Output:
{
"a": {
"a1": "1",
"a2": "2"
},
"b": {
"b1": "1",
"b2": "5"
}
}
With this approach you must have the key in the args input. If the key is missing, it will not be substituted with the default. To make it work even with not-present keys you need to use a third structure with all the possible path for example.
i have a javascript object like this:
cardControlEditFormData= {
cardBlocked: true,
posForeignLimit: 1000,
posGrLimit: 1010,
cnpGrLimit: 1010,
posGrPreviousLimit: 0,
posForeignPreviousLimit: 0
}
So i have to create a new object with a specific condition. My code is here:
export const prepareCardControlFormData = cardControlEditFormData => Object.keys(cardControlEditFormData).reduce((cardControlData, key) => {
if (key === 'cardBlocked') {
// eslint-disable-next-line no-param-reassign
cardControlData.cardBlocked = cardControlEditFormData.cardBlocked !== EMPTY_STRING;
} else if (key === 'posGrLimit') {
// eslint-disable-next-line no-param-reassign,max-len
cardControlData.posGrLimit = cardControlEditFormData.posGrLimit ? (cardControlEditFormData.posGrPreviousLimit || cardControlEditFormData.posGrLimit) : CARD_CONTROL_ZERO_VALUE;
} else if (key === 'posForeignLimit') {
// eslint-disable-next-line no-param-reassign,max-len
cardControlData.posForeignLimit = cardControlEditFormData.posForeignLimit ? (cardControlEditFormData.posForeignPreviousLimit || cardControlEditFormData.posForeignLimit) : CARD_CONTROL_ZERO_VALUE;
} else if (key === 'cnpGrLimit') {
// eslint-disable-next-line no-param-reassign,max-len
cardControlData.cnpGrLimit = cardControlEditFormData.cnpGrLimit ? (cardControlEditFormData.cnpPreviousGrLimit || cardControlEditFormData.cnpGrLimit) : CARD_CONTROL_ZERO_VALUE;
}
return cardControlData;
}, {})
;
But the eslint throw me an exception that i should not reassign parameter, how to write this in a more quality based way without this exception
cardControlData is a parameter for reduce() function and you are assigning values to its properties which might be causing eslint to give error. According to eslint docs, if you have the following setting it also gives error.
"eslint no-param-reassign: ["error", { "props": true }]"
Changing props to false will prevent eslint from giving error at assignments to properties.
Check props section for more details.
https://eslint.org/docs/rules/no-param-reassign#options
trying to delete properties from object its throwing error if it does not have value what is correct way to delete similar properties from multiple objects.
main.js
if (data.details.primary.transactionHistory !== undefined ||
data.details.secondary.transactionHistory !== undefined) {
delete data.details.primary.transactionHistory;
delete data.details.secondary.transactionHistory;
}
You need AND condition instead of OR. In your logic, delete operation will get fired if either case is true. Just replace || with &&
if (data.details.primary.transactionHistory !== undefined &&
data.details.secondary.transactionHistory !== undefined) {
delete data.details.primary.transactionHistory;
delete data.details.secondary.transactionHistory;
}
Edit 1:
If you have nested object, it is better to check properties step-by-step.
if (data.details && data.details.primary && data.details.primary.transactionHistory !== undefined) {
delete data.details.primary.transactionHistory
}
Now event if primary is not a property of details, your code will not fail.
You can simplify accordingly.
I'm not sure why you're checking whether transactionHistory is not undefined
Try this:
const details = data.details;
if (details) {
if(details.primary) {
details.primary.transactionHistory = null;
// or
delete details.primary.transactionHistory;
}
if(details.secondary) {
details.secondary.transactionHistory = null;
// or
delete details.secondary.transactionHistory;
}
}
This will essentially do the same thing that you're trying to achieve here.
If you're doing this frequently, you should generalize it:
function clean_up(objects, keys) {
objects.forEach(o => {
keys.forEach(k => {
try {
let base = k.split('.');
let del = base.pop();
let host = base.reduce((o, k) => o[k], o);
delete host[del];
} catch (e) {}
});
});
return objects;
}
console.log(
clean_up([{a: {b: 5}}, {z: 3, b: {} }, {c: 8}], ['a.b', 'c'])
);
If your want to delete transactionHistory in general and you don't want to create if-statements for each nested object within data.details, you can do:
// Data for snippet
let data = {
details: {
primary: { transactionHistory: 'blah' },
secondary: { transactionHistory: 'blah', someOtherProp: 'still here' },
nohistory: {},
miscproperties: { shouldNotBeDeleted: true }
}
}
// Deletion of transactionHistory from data
Object.keys(data.details).map((item) => { delete data.details[item].transactionHistory })
console.log(data)
You can add whatever conditionals you need within the open and closing brackets if necessary.
Is there a known way or a library that already has a helper for assessing whether an object is serializable in JavaScript?
I tried the following but it doesn't cover prototype properties so it provides false positives:
_.isEqual(obj, JSON.parse(JSON.stringify(obj))
There's another lodash function that might get me closer to the truth, _.isPlainObject. However, while _.isPlainObject(new MyClass()) returns false, _.isPlainObject({x: new MyClass()}) returns true, so it needs to be applied recursively.
Before I venture by myself on this, does anybody know an already reliable way for checking if JSON.parse(JSON.stringify(obj)) will actually result in the same object as obj?
function isSerializable(obj) {
var isNestedSerializable;
function isPlain(val) {
return (typeof val === 'undefined' || typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number' || Array.isArray(val) || _.isPlainObject(val));
}
if (!isPlain(obj)) {
return false;
}
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
if (!isPlain(obj[property])) {
return false;
}
if (typeof obj[property] == "object") {
isNestedSerializable = isSerializable(obj[property]);
if (!isNestedSerializable) {
return false;
}
}
}
}
return true;
}
Recursively iterating over all of given object properties. They can be either:
plain objects ("an object created by the Object constructor or one with a [[Prototype]] of null." - from lodash documentation)
arrays
strings, numbers, booleans
undefined
Any other value anywhere within passed obj will cause it to be understood as "un-serializable".
(To be honest I'm not absolutely positive that I didn't omit check for some serializable/non-serializable data types, which actually I think depends on the definition of "serializable" - any comments and suggestions will be welcome.)
In the end I created my own method that leverages Underscore/Lodash's _.isPlainObject. My function ended up similar to what #bardzusny proposed, but I'm posting mine as well since I prefer the simplicity/clarity. Feel free to outline pros/cons.
var _ = require('lodash');
exports.isSerializable = function(obj) {
if (_.isUndefined(obj) ||
_.isNull(obj) ||
_.isBoolean(obj) ||
_.isNumber(obj) ||
_.isString(obj)) {
return true;
}
if (!_.isPlainObject(obj) &&
!_.isArray(obj)) {
return false;
}
for (var key in obj) {
if (!exports.isSerializable(obj[key])) {
return false;
}
}
return true;
};
Here is a slightly more Lodashy ES6 version of #treznik solution
export function isSerialisable(obj) {
const nestedSerialisable = ob => (_.isPlainObject(ob) || _.isArray(ob)) &&
_.every(ob, isSerialisable);
return _.overSome([
_.isUndefined,
_.isNull,
_.isBoolean,
_.isNumber,
_.isString,
nestedSerialisable
])(obj)
}
Tests
describe.only('isSerialisable', () => {
it('string', () => {
chk(isSerialisable('HI'));
});
it('number', () => {
chk(isSerialisable(23454))
});
it('null', () => {
chk(isSerialisable(null))
});
it('undefined', () => {
chk(isSerialisable(undefined))
});
it('plain obj', () => {
chk(isSerialisable({p: 1, p2: 'hi'}))
});
it('plain obj with func', () => {
chkFalse(isSerialisable({p: 1, p2: () => {}}))
});
it('nested obj with func', () => {
chkFalse(isSerialisable({p: 1, p2: 'hi', n: { nn: { nnn: 1, nnm: () => {}}}}))
});
it('array', () => {
chk(isSerialisable([1, 2, 3, 5]))
});
it('array with func', () => {
chkFalse(isSerialisable([1, 2, 3, () => false]))
});
it('array with nested obj', () => {
chk(isSerialisable([1, 2, 3, { nn: { nnn: 1, nnm: 'Hi'}}]))
});
it('array with newsted obj with func', () => {
chkFalse(isSerialisable([1, 2, 3, { nn: { nnn: 1, nnm: () => {}}}]))
});
});
}
Here's how this can be achieved without relying on 3rd party libraries.
We would usually think of using the typeof operator for this kind of task, but it can't be trusted on its own, otherwise we end up with nonsense like:
typeof null === "object" // true
typeof NaN === "number" // true
So the first thing we need to do is find a way to reliably detect the type of any value (Taken from MDN Docs):
const getTypeOf = (value: unknown) => {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
};
We can then traverse the object or array (if any) recursively and check if the deserialized output matches the input type at every step:
const SERIALIZATION_ERROR = new Error(
`the input value could not be serialized`
);
const serialize = (input: unknown) => {
try {
const serialized = JSON.stringify(input);
const inputType = getTypeOf(input);
const deserialized = JSON.parse(serialized);
const outputType = getTypeOf(parsed);
if (outputType !== inputType) throw SERIALIZATION_ERROR;
if (inputType === "object") {
Object.values(input as Record<string, unknown>).forEach((value) =>
serialize(value)
);
}
if (inputType === "array") {
(input as unknown[]).forEach((value) => serialize(value));
}
return serialized;
} catch {
throw SERIALIZATION_ERROR;
}
};
Here's my solution with vanilla JS using pattern matching. It correctly flags Symbol() keys as non-serializable, a problem I ran into with the other code listed here.
It's also nicely concise, and maybe a bit more readable.
Returns true if the parameters can be serialized to JSON, returns false otherwise.
const isSerializable = n => (({
[ !!"default" ]: () => false,
[ typeof n === "boolean" ]: () => true,
[ typeof n === "string" ]: () => true,
[ typeof n === "number" ]: () => true,
[ typeof n === "object" ]: () =>
! Object.getOwnPropertySymbols( n ).length &&
isSerializable( Object.entries( n ) ),
[ Array.isArray( n ) ]: () => ! n.some( n => ! isSerializable( n ) ),
[ n === null ]: () => true,
})[ true ])();