I have a string like this "(ReadAccountAccess || ReadContractAccess) && CreateAccountAccess"
And I have this object:
{
PermissionID: 1,
AccountID: 1,
ReadAccountAccess: true,
ReadContractAccess: true,
CreateAccountAccess: true,
}
How can I check this condition?
It is NOT recommended, but you could use with statement and evaluate the expression with that object as the scope.
function check(obj, condition) {
with(obj) {
return eval(condition)
}
}
const input={PermissionID:1,AccountID:1,ReadAccountAccess:true,ReadContractAccess:true,CreateAccountAccess:true};
console.log(
check(input, "(ReadAccountAccess || ReadContractAccess) && CreateAccountAccess"),
check(input, "ReadAccountAccess === false"),
check(input, "AccountID === 1")
)
Instead of saving the condition in a string, you could use a function which takes an object as an input and returns the condition expression. This is a much better of deferring the check.
const input={PermissionID:1,AccountID:1,ReadAccountAccess:true,ReadContractAccess:true,CreateAccountAccess:true};
const c1 = o => (o.ReadAccountAccess || o.ReadContractAccess) && o.CreateAccountAccess,
c2 = o => o.ReadAccountAccess === false,
c3 = o => o.AccountID === 1
console.log(
c1(input),
c2(input),
c3(input)
)
You can use eval() javascript function.
const obj = {
PermissionID: 1,
AccountID: 1,
ReadAccountAccess: true,
ReadContractAccess: true,
CreateAccountAccess: true,
};
const { PermissionID, AccountID, ReadAccountAccess, ReadContractAccess, CreateAccountAccess } = obj;
const opr = "(ReadAccountAccess || ReadContractAccess) && CreateAccountAccess";
console.log(eval(opr));
Related
I have a array of objects with the property status, where I want to return false IF
I have (zero slots with "return" or zero "ordered" status or 1 slot with "verification" or 1 "active" status)
const slots = [
{
status: 'void'
},
{
status: 'closed'
},
{
status: 'ordered'
},
{
status: 'verification'
},
{
status: 'active'
}
]
I don't want to make like
x = slots.filter((val) => val.status === 'verification' || val.status === 'active'
if (x.length > 1) return false
y = slots.filter((val) => val.status === 'return' && val.status === 'ordered')
if (y.length > 0) return false
Is there a way to do this more efficiently and in just one "function"?
As stated in the comments, using filter you'll still need to call the filter method more than once as your conditions are different, but you could combine it into one line like the following, with a short circuit on the conditional and (where left hand side evaluates to false) which in some cases could produce only one call to filter:
const slots = [
{
status: 'void'
},
{
status: 'closed'
},
{
status: 'ordered'
},
{
status: 'verification'
},
{
status: 'active'
}
];
function checkslots(slots){
return slots.filter((val) => val.status === 'verification' ||
val.status === 'active').length <= 1 &&
slots.filter((val) => val.status === 'return' &&
val.status === 'ordered').length <= 0;
}
console.log(checkslots(slots));
I’d be inclined to make an object out of slots and test the created properties. I think the logic is a little clearer like that, since the condition is quite complex.
const slots = [{
status: 'void'
},
{
status: 'closed'
},
{
status: 'ordered'
},
{
status: 'verification'
},
{
status: 'active'
}
]
function check(slots) {
const o = Object.fromEntries(slots.map(s => [s.status, true]))
if ((o.verification || o.active) && !o.return && !o.ordered) return false;
return true;
}
console.log(check(slots));
For x you can use a combination of Array#filter with Array#includes and for y a combination of Array#every and Array#includes methods as follows:
const
slots = [ { status: 'void' }, { status: 'closed' }, { status: 'ordered' }, { status: 'verification' }, { status: 'active' } ],
x = slots
.filter(
({status}) => !['verification','active'].includes(status)
).length === 1,
y = slots
.every(
({status}) => !['return', 'ordered'].includes(status)
);
console.log( x, y, x || y );
This approach calculates the count of each of the different statuses, and then applies the OR rules to those counts.
const slots = [{status:"void"},{status:"closed"},{status:"ordered"},{status:"verification"},{status:"active"}];
const c = slots.reduce((a,{status:s})=>(a[s]??=0, a[s]++, a),{});
const r = !( !c.return || !c.ordered || c.verification===1 || c.active===1 );
console.log(r);
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.
var list = "OVER_30 = true || NUM_OF_JACKETS >=3 || COUNT_TOTAL == 500";
var array = getList(); // array[0].OVER_30 = true, array[0].NUM_OF_JACKETS = 5, array[0].COUNT_TOTAL = 500;
if (array[0].OVER_30 = true || array[0].NUM_OF_JACKETS >=3 || array[0].COUNT_TOTAL == 500) { <--- What I want to accomplish
return true;
}
I have a string variable called list that contains the conditions.
How can I add array[0]. in front of each condition to combine the array and string?
var format = array[0]. + condition??
You still need eval, but you could destructure the object and perform eval.
This approach need a valid check with a comparison operator
OVER_30 == true
instead of an assignment operator with =.
const
array = [
{ OVER_30: true, NUM_OF_JACKETS: 5, COUNT_TOTAL: 500 },
{ OVER_30: false, NUM_OF_JACKETS: 2, COUNT_TOTAL: 400 }
],
list = "OVER_30 == true || NUM_OF_JACKETS >=3 || COUNT_TOTAL == 500",
result = array.map(({ OVER_30, NUM_OF_JACKETS, COUNT_TOTAL }) => eval(list));
console.log(result);
I could only find eval for you
You DO need to make sure the statements are correctly formatted with spaces and correct number of =
So here I use two maps and a some - if you have && instead of || you need every which will return one combined result, meaning [true,true,true] is true and any other combination is false
Using some will return one combined result, any true is true
const list = "OVER_30 === true || NUM_OF_JACKETS >= 3 || COUNT_TOTAL === 500";
const array = [
{ OVER_30 : true, NUM_OF_JACKETS : 5, COUNT_TOTAL : 500},
{ OVER_30: false, NUM_OF_JACKETS: 2, COUNT_TOTAL: 400 },
{ OVER_30: true, NUM_OF_JACKETS: 2, COUNT_TOTAL: 400 }
]
const tests = list.split("||");
const results = array.map(item => tests.map(test => {
const [name, oper, val] = test.trim().split(" ");
const statement = `${item[name]} ${oper} ${val}`
return eval(statement)
}).some(test => test)
)
console.log(results)
Change your operator from = (assignment) to == or === (comparison) for the first condition. Or just use OVER_30. Hence:
"OVER_30 || NUM_OF_JACKETS >=3 || COUNT_TOTAL === 500"
You can use new Function()() constructor and Array#some() method as follows:
list.split('||').some(cond => new Function(`return array[0].${cond}`)())
which returns true if at least one of the conditions is true.
var list = "OVER_30 || NUM_OF_JACKETS >=3 || COUNT_TOTAL === 500";
var array = [
{OVER_30:true, NUM_OF_JACKETS:5, COUNT_TOTAL:500},
{OVER_30:true, NUM_OF_JACKETS:0, COUNT_TOTAL:100},
{OVER_30:false, NUM_OF_JACKETS:2, COUNT_TOTAL:200},
{OVER_30:false, NUM_OF_JACKETS:1, COUNT_TOTAL:400}
];
for(let i in array) {
if( list.split('||').some(cond => new Function(`return array[${i}].${cond}`)()) ) {
console.log( 'returning true.' );
} else {
console.log( '....false!' );
}
};
list.split(' || ').forEach(cond => console.log( new Function(`return array[1].${cond}`)() ));
An approach without using eval but instead a simple parser and then evaluation:
//helper functions
const peek = arr => arr[arr.length - 1];
const isOperator = token => typeof token === "object";
//available operators
const operators = new Map([
[ "===", { precedence: 2, operation: (a, b) => a === b }],
[ ">=", { precedence: 2, operation: (a, b) => a >= b }],
[ "||", { precedence: 1, operation: (a, b) => a || b }],
]);
//convert into operators and operands
const tokenize = str => str.split(/\s+|\b/)
.map(token => operators.has(token) ? operators.get(token) : token );
//convert literal tokens and binary operators into reverse polish notation
const parse = tokens => {
const opStack = [];
const output = [];
for (const token of tokens) {
if (isOperator(token)) {
while(isOperator(peek(opStack)) && token.precedence <= peek(opStack).precedence) {
output.push(opStack.pop());
}
opStack.push(token);
} else {
output.push(token);
}
}
return output.concat(opStack.reverse());
};
const consume = (rpnTokens, obj) => {
const output = [];
for(const token of rpnTokens) {
if (isOperator(token)) {
const b = output.pop();
const a = output.pop();
const result = token.operation(a, b);
output.push(result);
} else {
const value = token in obj
? obj[token] //object properties - fetch from object
: JSON.parse(token); //others convert into values
output.push(value);
}
}
return output[0];
}
// ▲▲▲ code ▲▲▲
// ▼▼▼ usage ▼▼▼
const list = "OVER_30 === true || NUM_OF_JACKETS >= 3 || COUNT_TOTAL === 500";
const array = [
{ OVER_30: true, NUM_OF_JACKETS: 7, COUNT_TOTAL: 500 }, //true
{ OVER_30: false, NUM_OF_JACKETS: 7, COUNT_TOTAL: 500 }, //true
{ OVER_30: false, NUM_OF_JACKETS: 1, COUNT_TOTAL: 500 }, //true
{ OVER_30: false, NUM_OF_JACKETS: 1, COUNT_TOTAL: 100 }, //false
{ OVER_30: true, NUM_OF_JACKETS: 1, COUNT_TOTAL: 100 } //true
];
const tokens = tokenize(list);
const rpn = parse(tokens);
for (const item of array) {
console.log(consume(rpn, item));
}
The string is converted into tokens
"OVER_30 === true || NUM_OF_JACKETS >= 3 || COUNT_TOTAL === 500" will be represented as
[
"OVER_30", OP[===], "true",
OP[||],
"NUM_OF_JACKETS", OP[>=], "3",
OP[||],
"COUNT_TOTAL", OP[===], "500"
]
Afterwards a simplified version of the shunting-yard algorithm re-orders the tokens into reverse Polish notation producing:
[
"OVER_30", "true", OP[===],
"NUM_OF_JACKETS", "3", OP[>=],
OP[||],
"COUNT_TOTAL", "500", OP[===],
OP[||]
]
The list is consumed by going through it one by one.
3.1. Any operand is converted to a value and added to a stack:
something that exists on an object as a property is converted by getting the value from the object
anything else is assumed to be a JavaScript primitive and converted to a value
3.2. Operators are applied to the last two values and the result is put on the stack.
In the end, there is a single result on the stack and that is the result of the evaluation of all tokens.
This assumes a well formed and logical string is given.
The solution can now handle any valid expression and can work with literals. It can be extended with more operators by just adding them to the configuration:
//helper functions
const peek = arr => arr[arr.length - 1];
const isOperator = token => typeof token === "object";
//available operators
const operators = new Map([
[ "+", { precedence: 3, operation: (a, b) => a + b }], //support addition
[ "===", { precedence: 2, operation: (a, b) => a === b }],
[ ">=", { precedence: 2, operation: (a, b) => a >= b }],
[ "||", { precedence: 1, operation: (a, b) => a || b }],
]);
//convert into operators and operands
const tokenize = str => str.split(/\s+|\b/)
.map(token => operators.has(token) ? operators.get(token) : token );
//Shunting-yard algorithm for literal tokens and binary operators into reverse polish notation
const parse = tokens => {
const opStack = [];
const output = [];
for (const token of tokens) {
if (isOperator(token)) {
while(isOperator(peek(opStack)) && token.precedence <= peek(opStack).precedence) {
output.push(opStack.pop());
}
opStack.push(token);
} else {
output.push(token);
}
}
return output.concat(opStack.reverse());
};
const consume = (rpnTokens, obj) => {
const output = [];
for(const token of rpnTokens) {
if (isOperator(token)) {
const b = output.pop();
const a = output.pop();
const result = token.operation(a, b);
output.push(result);
} else {
const value = token in obj
? obj[token] //object properties - fetch from object
: JSON.parse(token); //others convert into values
output.push(value);
}
}
return output[0];
}
console.log(consume(parse(tokenize("5 >= 6")), {})); //false
console.log(consume(parse(tokenize("7 >= 6")), {})); //true
console.log(consume(parse(tokenize("4+1>=6")), {})); //false
console.log(consume(parse(tokenize("4+3>=6")), {})); //true
console.log(consume(parse(tokenize("1+1+1 === 3")), {})); //true
console.log(consume(parse(tokenize("true || false")), {})); //true
console.log(consume(parse(tokenize("false || false")), {})); //false
console.log(consume(parse(tokenize("apples === oranges+2")), //true
{ apples: 42, oranges: 40})
);
What I am trying to achieve here is that, I made a simple utility function and that should return 'true' or 'false', based on the given argument to the isEmpty method.
// The below log should return 'false', But it's returning 'true'
console.log(isEmpty( () => {key: 1} ));
What I tried so far
function isEmpty(value) {
const type = typeof value;
if ((value !== null && type === 'object') || type === 'function') {
const properties = Object.keys(value);
return properties.length === 0 || properties.size === 0
}
return !value;
}
And it's working for below cases
console.log(isEmpty( {} )) // true
console.log(isEmpty( [] )) // true
console.log(isEmpty( Object.create(null) )) // true
console.log(isEmpty( null )) // true
console.log(isEmpty( '' )) // true
console.log(isEmpty( {key: 1} )) // false
console.log(isEmpty( [1,2,3] )) // false
But it's not working, when we get the return object / array from the function
console.log(isEmpty( () => ({key: 1}) ))
console.log(isEmpty( () => ([1,2,3]) ))
Using Object.keys() on a function will always result in an empty array (no matter the return type). This is due to the fact that the keys of a function are not enumerable (the keys being name and length), and Object.keys() will only give back the keys which can be enumerated. This means the length of the array of keys will always be 0, meaning your function will give back true even though the function it is passed returns a non-empty value.
If you can invoke the value (if it is a function), it will allow you to get the returned value out of it (ie: an object or array), and then recurse with your function that you currently have:
function isEmpty(value) {
const type = typeof value;
if (value !== null && type === 'object') {
const prototype = Object.getPrototypeOf(value) || {};
const properties = Object.keys(value) + Object.keys(prototype);
return properties.length === 0 || properties.size === 0
} else if(type === 'function') {
const res = value();
return isEmpty(res);
}
return !value;
}
console.log(isEmpty( {} )) // true
console.log(isEmpty( [] )) // true
console.log(isEmpty( Object.create(null) )) // true
console.log(isEmpty( null )) // true
console.log(isEmpty( '' )) // true
console.log(isEmpty(() => ({}))); // true
console.log(isEmpty(() => () => () => ({}))); // true
console.log(isEmpty( {key: 1} )) // false
console.log(isEmpty( [1,2,3] )) // false
console.log(isEmpty(() => ({key: 1}))); // false
console.log(isEmpty( () => ([1,2,3]) )) // false
console.log(isEmpty(() => (Object.create({key: 1})))) // false
In order to achieve that you will have to check the type of the value and if it is a function then invoke to get the result.
If the function passed have side effects they would be called, which could cause some issues.
function isEmpty(value) {
const type = typeof value;
// If passed a function, invoke it and get the result
if (type === 'function') {
value = value();
}
if (value && (type === 'object' || type === 'function')) {
const properties = Object.keys(value);
return properties.length === 0 || properties.size === 0
}
return !value;
}
When you call a function like () => { key: 1 } you are actually creating a function which looks like
function {
key: 1
}
which means that the function do not have a return value. Instead you should use it like this () => ({ key: 1 }) which will create a
function like:
function {
return { key: 1 }
}
https://babeljs.io/repl#?babili=false&browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=MYewdgzgLgBAYgVzMAjDAvDAFASgwPhgG8YBrAUwE8AuGNAXwChRJZFkAmDbPdQrEhRp0Y9HEA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=es2015%2Creact%2Cstage-2&prettier=false&targets=&version=7.7.3&externalPlugins=
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 ])();