Flow typing issue on conditional object property - javascript

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.

Related

How to dynamically update an object field in JavaScript/TypeScript

I have a method for handling an update on this object. In this method I want to accept any field value that the object has, such as name, age, weight, eyeColor etc. If that field exists within that object, I'd like to be able to update the object dynamically for any field they pass in.
I am currently doing it incorrectly I believe with the spread operator while trying to update one field in the object. There is an error that fieldName does not exist within myObject. Does anyone know how to do this dynamically without need for a long switch statement checking each of the fields against fieldName passed in? In previous attempts tweaking with this I have successfully added a new field to the object named "fieldName" which is also not what I wanted.
If anyone has any ideas, that would be so helpful, thank you!
let myObject = {
name: 'John',
lastName: 'Smith',
age: 26,
weight: 200,
eyeColor: 'blue',
hairColor: 'blonde'
};
const handleUpdate = (fieldName: string, fieldValue: string | number) => {
if (fieldName in myObject) {
myObject = {...myObject, fieldName: fieldValue};
}
}
handleUpdate('name', 'Jack'); // Want this call to update the name of 'John' to 'Jack'
In short, you're looking for:
{...myObject, [fieldName]: fieldValue}
You can make a generalized, typesafe function to do this as follows:
function updateProp<TObj, K extends keyof TObj>(obj: TObj, key: K, value: TObj[K]) {
return {...obj, [key]: value};
}
and call it as follows:
const foo = { a: 1, b: "monkey" };
const updatedFoo = updateProp(foo, "b", "hello world")
Playground Link
You're looking for the Bracket notation property accessor:
myObject[fieldName] = fieldValue
Compared to the approach with the spread operator, this does actually update the object in place. I.e. if the reference in myObject was previously copied elsewhere, that reference will also "see" the updated field.
Whereas, by overriding the value with myObject = {...myObject}, you're creating a new object each time.

Mongoose automatically change the type of the value

In mongoose.model, I have chosen the type of name to be a string and the type of age to be a number, but when I enter a number as the value of name, I don't get an error and the same thing happens when I use something like '18' as the value of age.
Here is the code:
const User = mongoose.model('User', {
name: { type: String },
age: { type: Number }
});
const me = new User({
name: 12,
age: '18'
});
me.save().then(() => console.log(me)).catch(error => console.log(error));
Mongoose casts the values to the corresponding type, if it fails a CastError is thrown, from the doc:
Before running validators, Mongoose attempts to coerce values to the
correct type. This process is called casting the document. If casting
fails for a given path, the error.errors object will contain a CastError object.
You can try this by given age the value 'aa' for example.
If you want to override this behavior you can use one of the following options:
Disable casting globally: mongoose.Number.cast(false)
Disable casting just for a given path:
age: {
type: Number,
cast: false // Disable casting just for this path
},
Use a custom function:
age: {
type: Number,
cast: v => { return typeof v === 'number' && !isNaN(v) ? Number(v) : v; } // Override casting just for this path
}

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

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.

Removing key in a JSON object but want to keep the original

I am trying to delete a key in variable a. However, i still want to keep the original variable value for another purpose. But somehow its alway overwrite the initial a.
This is the code that i wrote:
let a = [
{ firstName: 'Tony', lastName: 'Stack' },
{ firstName: 'Iron', lastName: 'Man' }
];
console.log('before',a);
delete a[0].firstName;
console.log('after',a);
Output:
I am expecting the first console.log will be printing out the initial value.
It's just confuse myself. I feel like i must have missed some knowledge here. Appreciate for any help. Thanks
You can use map and spread operator like below
let a = [
{ firstName: 'Tony', lastName: 'Stack' },
{ firstName: 'Iron', lastName: 'Man' }
];
let removed = a.map(({firstName, ...lastName}) => lastName);
console.log('before:',a)
console.log('after:', removed)
Well your question has two parts. First, the console.log showing the same value before and after. What happens is, Chrome's console doesn't like to keep track of complicated data like objects. So, when you log the variables with the console closed, Chrome will only remember the variable. Then, when you open the console it will read that variable and display it in the logs. However, if you leave the console open and refresh the page you should see the correct values being printed.
The second part, storing the object to use it for later, requires something known as deep cloning. Here is the reason why that is required and here is a post detailing how to do it.
Your code is working correctly. Refer here for a full answer. You can verify your answer by logging the firstName instead of the whole object.
let a = [
{ firstName: 'Tony', lastName: 'Stack' },
{ firstName: 'Iron', lastName: 'Man' }
];
console.log('before',a[0].firstName);
delete a[0].firstName;
console.log('after',a[0].firstName);
let a = [
{ firstName: 'Tony', lastName: 'Stark' },
{ firstName: 'Iron', lastName: 'Man' }
];
let b= {...a} // use spread operator to clone an array
console.log('before',a);
delete b[0].firstName;
console.log('after',b);

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. ☠️

Categories

Resources