Is using undefined as a value in object definition a good practice? - javascript

tl;dr: Is it a good practice to use undefined as a value or should I avoid it and try with another aproach?
I have an object which I use as a schema for my two functions createUser() and updateUser() and based on what values I need, I reconfigure it.
For updateUser() I need to send only the keys user entered in a form so the only way I know of, without changing the structure of the object manually, is to set the values to undefined.
// d is passed as argument
const args = {
variables: {
where: {id: "someValue"},
data: {
username: d.username || undefined,
password: d.password || undefined,
role: d.role || undefined,
},
},
};
Now if I have entered only username, my object will be
variables: {
where: { id: "someValue"},
data: { username: "anotherValue" }
}
I have given it a second thought after ESLint gave me a warning "Unexpected use of undefined."
NOTE I can't send empty values to API. It has to have either value or not send the key at all.

const args = {
variables: {
where: {id: "someValue"},
data: {
username: d.username || "",
password: d.password || "",
role: d.role || "",
},
},
};
It makes more sense to use empty values instead of keeping them undefined, or you can use null.
So, that your API contracts won't get violated.

It's difficult to determine what good is and isn't, because it's always all about the needs and preferences of the client and your teammates.
The very simple and short answer to the question is: yes. undefined is a valid value and if this would be an evidently bad practice, then the language would not allow that value to be assigned. However, it's important to make sure that you do not duplicate your values. Taking a look at this object
{
variables: {
where: {id: "someValue"},
data: {
username: d.username || undefined,
password: d.password || undefined,
role: d.role || undefined,
},
},
};
we see that you repeat the same idea over and over again. Instead, you would do better to implement something like this:
function nicify(object) {
for (var key in object) {
if (!object[key]) object[key] = undefined;
else if ((typeof(object[key]) === "object") || (Array.isArray(object[key]))) {
nicify(object[key]);
}
}
}
the function above recursively does what you wanted to do with your attributes. This will be very helpful if you have many attributes and/or many use-cases. Also, if you consistently have the pattern of having a source object as in your example, then you can implement something like this:
function fillBySource(object, source) {
for (var key in source) {
object[key] = source[key] || undefined;
}
}

I think || null or || '' are better practise, if you JSON.stringify() to exchange data with a server or something nothing in the JSON tell you that a username and a password should be present in the data prop. You can see that in the following eg :
function test(username, password, role) {
const args = {
variables: {
where: {id: "someValue"},
data: {
username: username || undefined,
password: password || undefined,
role: role || undefined,
},
},
};
return JSON.stringify(args);
}
let json = test();
console.log(json);

To my best of knowledge, you should not assign undefined the way you are currently doing it, because if d.username in the code is not set, then it's value is already undefined.
Usually the use case for assigning undefined is when a variable is initially set, but you want to unset/reset it as if the value was never set in the first place.

Related

chain logical AND without fixed length given an array in Node.js and Typescript

Scenario:
I am making a generic function that returns a boolean depending on logical AND statements, however, the function being generic accept multiple type of objects and arrays, and the statements can vary depending on the objects.
at the moment I have something like this
private async myFunction(
myArray: myArrObj[],
myObj : myObj,
): Promise<boolean> {
return (
myArr.some(
(a) =>
a.status1=== "*" ||
a.status1 === myObj.status1.status1Id
) &&
myArr.some(
(a) =>
a.status2=== "*" ||
a.status2 === myObj.status2.status2Id
) &&
myArr.some(
(a) =>
a.status3=== "*" ||
a.status3 === myObj.status3.status3Id
) &&
myArr.some(
(a) =>
a.status4=== "*" ||
a.status4 === myObj.status4.status4Id
)
)
}
the issue is not being able to know what kind of array is passed and how many checks are needed, how can I make a return? My idea was storing each array.some method in an array and join them with " && ", this approach would require to execute something from a string, which I'm not sure is the most secure thing to do, since eval is not secure at all.
to get the myObj statuses I could just use a for loop and store the the the property in a string.
I can't come up with a good solution, so feel free to propose something new if my idea is not good enough
As noted by others in the comments, it would help if you had a reproducible example with sample data. That being said, from your comment:
but the statuses and id's have different names, some id's are .nameId, and some are just .id , but the statuses themselves have the same name, so instead of status1 and obStatus1 it really should be status1 and status1
Breaking this down:
but the statuses and id's have different names, some id's are .nameId, and some are just .id
You could try to see if nameId exists and fall back to id.
but the statuses themselves have the same name, so instead of status1 and obStatus1 it really should be status1 and status1
When myArr entries share keys with myObj, then you could simply loop through myObj's keys.
async function myFunction(myArr, myObj) {
// Fallback value for if .nameId and .id both don't exist.
// Falling back to `undefined` would cause a bug / false positives.
const notFound = Symbol();
// Loop through every key:value pair in the input object.
return Object.entries(myObj).every(([myObjKey, myObjValue]) => {
// Handle both `.nameId` and `.id`
const id = myObjValue[`${myObjKey}Id`] ?? myObjValue.id ?? notFound;
// If `myArrObj`'s children only ever contain exactly
// a single key { status2: { someRandomKey: 123 } }, then you
// could use myObjValue[Object.keys(myObjValue)[0]];
// For this key--for example "status1"--is there *any* array entry
// in `myArrObj` that has the same key and value or "*"?
return myArr.some((a) => {
return a[myObjKey] === '*' || a[myObjKey] === id;
});
});
}
With the following sample data:
const sampleArr = [
{ status3: "*" },
{ status2: 234 },
{ status1: 123, thisIsAnUnusedKey: true },
{ status4: 456 },
{ name: "Foobar" },
{ thisIsAnUnusedArrayEntry: true },
];
const sampleObj = {
status1: {
status1Id: 123,
},
status2: {
status2Id: 234,
},
status3: {
status3Id: 345,
},
status4: {
// Different key
id: 456,
},
name: {
// Different dataType
nameId: "Foobar"
}
};
myFunction(sampleArr, sampleObj).then(console.log); // Logs `true`

Flow typing issue on conditional object property

I'm having this flow issue string [1] is not an object with the following code:
type User = {
name: string,
age: number,
gender?: string,
}
const user: User = {
name: 'xxx',
age: 23,
...(props.gender && { gender: props.gender }) // <----- the culprit is this line
}
Do you know why?
Seems like conditionally setting a key is not correctly supported with the object rest spread operator.
I solved it doing:
const user: User = {
name: 'xxx',
age: 23,
}
if (props.gender) {
user.gender = props.gender
}
but I don't want to loose a language feature based on the flow typing.
Flow is catching a legitimate type error bug in your code here. If gender were an empty string, your code equates to
const user: User = {
name: 'xxx',
age: 23,
...""
};
which, while not a runtime error, is definitely a weird type error. You should only ever use object spread syntax on an object.
The best fix here would either be to assign the property after, as you've done, or to do
...(props.gender ? { gender: props.gender } : null)
which clearly says "if gender is falsy, don't spread any properties". You could also use {} instead of null, but that just creates an extra object for no reason.
Reading through this Github issue on object rest spread operator I found a better way to solve it. And this is by defaulting to an empty object.
So I changed this line:
...(props.gender && { gender: props.gender })
with
...((props.gender && { gender: props.gender }) || {})
and now the flow error is resolved.

ES6, parameter destructuring with default values. A way to write less

Ho can I avoid to write all fields with = ""?
const defaultPlayer = {
name: "",
surname: "",
age: "",
skill: ""
}
// ...
mapPropsToValues = ({ player }) => player || defaultPlayer
Is there in javascript that I can use to avoid write all the time = ""?
I mean, if I already know that defalut value of every field is "" (empty string) how can I do instead of write every field explicitly?
You can accomplish this using a with block and a Proxy object and eval:
let defaultPlayer;
with (new Proxy({}, {
has(o, key) {
try { eval(key); }
catch (e) { return true; }
},
get() {
return '';
}
})) {
defaultPlayer = {
userName,
age,
surname,
isAdmin: false
};
}
console.log(defaultPlayer);
When you omit a property value, it looks for a variable with the same name as the key. For example, {x} is the same as {x: x}. We can use the with() statement to have JavaScript check for properties of an object before looking for variables. Instead of a normal object, we'll define a Proxy that returns '' for any variables that it doesn't see in the local environment (by testing for an exception when evaling their name).
☠️ This is an obscene hack with nasty edge cases. Never use it. ☠️

Typescript - if conditrional inside a map

I am mapping a subset of user data to an object of a refined data set. Inside the map i want to check if a variable is null or undefined, and if yes, then to set this variable to a placeholder value.
The issue I am facing is that declaring an if statement inside the map is causing an error, but even though a map can have an index as a parameter, how can we use it functionally with a conditional Statement? Any insight most appreciated.
return this.UserService.search(url)
.map((data) => {
console.log(data);
data.result = <any> data.result.map((user, index) => ({
// if statement causing error here
if(user.name === null || user.name === undefined){
// error with this if condition
},
id: user.id,
name: user.name,
type: user.type,
password: user.password,
}));
return data;
}).map(data => ({
meta: { totalItems: data.size },
data: data.result,
}));
You're attempting to use an object literal as the return type, but naturally, an if statement (or any statement) can't be inside object literal syntax.
So instead, define a function body, which also uses curly braces, and put your code inside with an explicit return statement.
// Starts function body instead of single expression-v
data.result = <any> data.result.map((user, index) => {
if (some_condition) {
return "some value"; // The object?
} else {
return "other value"; // A different object?
}
/*
// I assume these are to be used in the object you return
id: user.id,
name: user.name,
type: user.type,
password: user.password,
*/
});
You can express conditions in literal maps, but it is somewhat ugly.
return {
a: 1,
...(some_condition && {
b: 1,
})
};
As far as i know you can't do that with JUST a map.
however you could follow it up with a filter() function:
const newArray = oldArray.map((value, index) => condition ? value : null).filter(v => v);
you basicaly iterate over each item and then return the value or null depending on your condition.
Now once you have the map you just filter it by removing the null values from the array.
Notice that the original array is not altered and a new one is returned.
thanks for the idea #user8897421 for the idea. i just wanted to turn it into a one liner.

Why can't I delete a mongoose model's object properties?

When a user registers with my API they are returned a user object. Before returning the object I remove the hashed password and salt properties. I have to use
user.salt = undefined;
user.pass = undefined;
Because when I try
delete user.salt;
delete user.pass;
the object properties still exist and are returned.
Why is that?
To use delete you would need to convert the model document into a plain JavaScript object by calling toObject so that you can freely manipulate it:
user = user.toObject();
delete user.salt;
delete user.pass;
Non-configurable properties cannot be re-configured or deleted.
You should use strict mode so you get in-your-face errors instead of silent failures:
(function() {
"use strict";
var o = {};
Object.defineProperty(o, "key", {
value: "value",
configurable: false,
writable: true,
enumerable: true
});
delete o.key;
})()
// TypeError: Cannot delete property 'key' of #<Object>
Another solution aside from calling toObject is to access the _doc directly from the mongoose object and use ES6 spread operator to remove unwanted properties as such:
user = { ...user._doc, salt: undefined, pass: undefined }
Rather than converting to a JavaScript object with toObject(), it might be more ideal to instead choose which properties you want to exclude via the Query.prototype.select() function.
For example, if your User schema looked something like this:
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
},
name: {
type: String,
required: true
},
pass: {
type: String,
required: true
},
salt: {
type: String,
required: true
}
});
module.exports = {
User: mongoose.model("user", userSchema)
};
Then if you wanted to exclude the pass and salt properties in a response containing an array of all users, you could do so by specifically choosing which properties to ignore by prepending a minus sign before the property name:
users.get("/", async (req, res) => {
try {
const result = await User
.find({})
.select("-pass -salt");
return res
.status(200)
.send(result);
}
catch (error) {
console.error(error);
}
});
Alternatively, if you have more properties to exclude than include, you can specifically choose which properties to add instead of which properties to remove:
const result = await User
.find({})
.select("email name");
The delete operation could be used on javascript objects only. Mongoose models are not javascript objects. So convert it into a javascript object and delete the property.
The code should look like this:
const modelJsObject = model.toObject();
delete modlelJsObject.property;
But that causes problems while saving the object. So what I did was just to set the property value to undefined.
model.property = undefined;
Old question, but I'm throwing my 2-cents into the fray....
You question has already been answered correctly by others, this is just a demo of how I worked around it.
I used Object.entries() + Array.reduce() to solve it. Here's my take:
// define dis-allowed keys and values
const disAllowedKeys = ['_id','__v','password'];
const disAllowedValues = [null, undefined, ''];
// our object, maybe a Mongoose model, or some API response
const someObject = {
_id: 132456789,
password: '$1$O3JMY.Tw$AdLnLjQ/5jXF9.MTp3gHv/',
name: 'John Edward',
age: 29,
favoriteFood: null
};
// use reduce to create a new object with everything EXCEPT our dis-allowed keys and values!
const withOnlyGoodValues = Object.entries(someObject).reduce((ourNewObject, pair) => {
const key = pair[0];
const value = pair[1];
if (
disAllowedKeys.includes(key) === false &&
disAllowedValues.includes(value) === false
){
ourNewObject[key] = value;
}
return ourNewObject;
}, {});
// what we get back...
// {
// name: 'John Edward',
// age: 29
// }
// do something with the new object!
server.sendToClient(withOnlyGoodValues);
This can be cleaned up more once you understand how it works, especially with some fancy ES6 syntax. I intentionally tried to make it extra-readable, for the sake of the demo.
Read docs on how Object.entries() works: MDN - Object.entries()
Read docs on how Array.reduce() works: MDN - Array.reduce()
I use this little function just before i return the user object.
Of course i have to remember to add the new key i wish to remove but it works well for me
const protect = (o) => {
const removes = ['__v', '_id', 'salt', 'password', 'hash'];
m = o.toObject();
removes.forEach(element => {
try{
delete m[element]
}
catch(O_o){}
});
return m
}
and i use it as I said, just before i return the user.
return res.json({ success: true, user: await protect(user) });
Alternativly, it could be more dynamic when used this way:
const protect = (o, removes) => {
m = o.toObject();
removes.forEach(element => {
try{
delete m[element]
}
catch(O_o){}
});
return m
}
return res.json({ success: true, user: await protect(user, ['salt','hash']) });

Categories

Resources