I'm having a problem with types in typescript.
So i have an array of som ID's i get from some checkboxes. This array could also be empty.
example of values that can be returned from submit():
const responseFromSubmit = {
1: {
id: "1",
value: "true"
},
2: {
id: "2",
value: "false"
},
3: {
id: "3",
value: "false"
}
};
const Ids: number[] = Object.values(submit()!)
.map(formfield => {
if (formfield.value === 'true') {
return Number(formfield.id);
}
})
.filter(id => id != undefined);
So in this case the Ids would be Ids = [1].
I have tried several solution like trying to change the value of Ids after the codeblock above by checking if Ids is undefined:
if (ids.length > 0){
ids = []
}
Through this code the constant Ids is type of (Number | undefined)[], how can i make it always be of type number[] even if its empty?
Here is a solution, but I dont like it at all:
const Ids: number[] = Object.values(submit()!)
.map(formfield => {
if (formfield.value === 'true') {
return Number(formfield.id);
} else {
return 0;
}
})
.filter(id => id != 0);
In my case the formfield.id will never have value 0, so it is possible to filter all elements with the value 0. So I would not recomend this solution. but hey, it works, right? ¯\_(ツ)_/¯
The problem
The main issue is the .filter() signature. It will always return an array of the same type as what you begun with. It is not possible for the TypeScript compiler to guaranteed anything else. Here is an example:
const arr/*: (string | number) */ = ["one", 2, 3, "four", 5, 6];
const numbers/*: number[]*/ = arr.filter(x => typeof x === "number");
console.log(numbers);
Playground Link
This works if you disregard types but it's functionally equivalent to the following:
const arr/*: (string | number)[]*/ = ["one", 2, 3, "four", 5, 6];
const numbers/*: number[]*/ = arr.filter(x => x !== "one");
console.log(numbers);
Playground Link
In both cases you have an array of mixed types and some filtering function. In order to guarantee that the result would be only a specific type, you need to examine the code and make inferences. This is not how the compiler works, however - calling .filter() on Array<T | U> can only produce Array<T | U> again, the generic is unchanged.
Solution
What you can do is flip the order of your .map and .filter. You'll need to re-write them but it will work correctly in term of types. I also makes the logic more coherent - right now you are double filtering implicitly. The map() will only transform some types, not others, thus doing an indirect filter. The actual .filter() call then sieves the unmapped/soft-filtered values.
The correct logic and correct type preservation would thus be the following:
const Ids: number[] = Object.values(submit()!)
.filter(formfield => formfield.value === 'true')
.map(formfield => Number(formfield.id))
Playground Link
This is shorter and more correct form of the logic you want.
The real filtering condition formfield.value === 'true' is extracted by itself in the .filter() call.
.filter() runs first so you're guaranteed to have the have the same types from compiler perspective and you've just shrunk the list to only the items you're interested in.
.map() not does exactly what it's meant to - a 1:1 mapping for each value of the array. It doesn't need to do any logic more complex. So, it doesn't need to concern itself with what is or isn't correct in order to carry out the transformation.
Try to add:
if (formfield.value === 'true') {
return Number(formfield.id);
}
return null;
under the return in the if.
The complete code:
const Ids: number[] = Object.values(submit()!)
.map(formfield => {
if (formfield.value === 'true') {
return Number(formfield.id);
}
return null;
})
.filter(id => id != undefined);
EDIT:
A better way to check if one variable is undefined is with typeof operator:
typeof id !== 'undefined'
I can't answer your question directly, but allow me to suggest a different approach:
const ids = Object.keys( obj || {} )
.reduce( function(acc,cur) {
if( obj[cur] ) acc.push(cur);
return acc
},
[]
)
It sounds like you want to extract a subset of values from your object. This will return the keys that are truthy. Instead of acc.push(cur) you would want to push the equivalent of Number(formfield.id). The obj || {} at the top allows 'obj' to be undefined.
Related
I'm using angular 7 and trying to do something in a child component : use the input property that can be at first level in the object or deeper.
My child component has this piece of code :
if (this.values.filter(obj => obj[this.matchPropertyName] === $event[i].id).length === 0) {
...
}
where this.matchPropertyName is my input (can be 'id', 'myProperty.id',...)
For a single level (obj.id) this code works. However, I need to use sometimes from a deeper level (obj.myProperty.id) and it doesn't work.
How can I achieve this ?
Let me know if it not clear enough.
I'm using angular 7 and typescript 3.2.4
I don't think there's a built-in solution but you can make use of a simple split and reduce. For example:
const value = this.matchPropertyName.split('.').reduce((pre, curr) => pre[curr], obj);
would give the value for obj.myProperty.id when this.matchPropertyName="myProperty.id"
Stackblitz
So in your case, you could use it like:
const theValue = this.matchPropertyName.split('.').reduce((pre, curr) => pre[curr], obj);
if (this.values.filter(obj => theValue === $event[i].id).length === 0) {
...
}
Final result by the OP:
myEventListener($event) {
if (this.values.filter(obj => this.resolveProperty(obj) === $event[i].id).length === 0) {
...
}
}
resolveProperty(obj) {
return this.matchPropertyName.split('.').reduce((pre, curr) => pre[curr], obj);
}
What is the best practice for me to sort an object of array by value of key that key is optional type from Object? I have following code that works if num is not optional type However, I got error that Object is possibly 'undefined'.
type Filter = {
a: number;
num?: number
}
const filter: Filter[] = [{a: 1, num: 10}, {a: 1, num: 12}, {a: 1, num: 5}]
filter.sort((a,b)=> (a.num > b.num ? 1 : -1))
console.log(filter)
2 options come to my mind:
The obvious one: use a type where num is not optional. You can use the Required utility type if you do not want to keep both types without rewriting the properties.
If you cannot change the type, that probably means you really want that property to be optional. So you simply should check for it in your sort function. You need to check if num is undefined and decide what to return when that occurs. When you check it, Typescript will infer everything that falls out of that condition will be defined.
For instance:
filter.sort((a,b)=> {
if (typeof a.num === 'undefined' || typeof b.num === 'undefined') {
return 0;
}
return (a.num > b.num ? 1 : -1)
})
To add to AqueleHugo's answer, I wanted to keep the field optional (to correctly reflect the 3rd party's interface), but still keep it clean, simple and readable, so I've gone with this and it works well for my situation:
interface WebPage {
// ...
order?: number;
}
const sortPages = (pages: WebPage[]): WebPage[] => {
return pages.filter(p => p.order).sort((p1, p2) => Number(p1.order) - Number(p2.order));
}
Hopefully someone else finds this useful!
I need to check if any object in an array of objects has a type: a AND if another has a type: b
I initially did this:
const myObjects = objs.filter(attr => attr.type === 'a' || attr.type === 'b');
But the code review complained that filter will keep going through the entire array, when we just need to know if any single object meets either criteria.
I wanted to use array.find() but this only works for a single condition.
Is there anyway to do this without using a for loop?
you can pass two condition as given below
[7,5,11,6,3,19].find(attr => {
return (attr > 100 || attr %2===0);
});
6
[7,5,102,6,3,19].find(attr => {
return (attr > 100 || attr %2===0);
});
102
Updated answer:
It's not possible to shortcircuit js's builtin functions that does what you want, so you will have to use some kind of loop:
let a;
let b;
for (const elm of objs) {
if (!a && elm === 'a') {
a = elm;
}
if (!b && elm === 'b') {
b = elm;
}
const done = a && b;
if (done) break;
}
Also you should consider if you can record a and b when producing the array if that's possible.
Oiginal answer:
`find` works just like `filter` where it takes a predicate, returns the first element that the predicate returns `true`.
If I understood your question correctly, you can just replace the `filter` with `find` and it will return at the first occurance:
const myObject = objs.find(attr => attr.type === 'a' || attr.type === 'b');
Also notice your provided snippet is wrong for what you described: `filter` returns an array but you only wanted one element. so you should add `[0]` to the filter expression if you want to use it.
I primarily work with React and often find that when I write a function that relies on a component's state, I have to perform a check to see if the piece of state is defined before performing any actions.
For example: I have a function that uses .map() to loop over an array of objects fetched from a database and generates jsx for each object in the array. This function is called in the render() function of my component. The first time render() is called, the initial array is empty. This results in an error because, of course, the first index of the array is undefined.
I have been circumventing this by making a conditional check to see if the value of the array is undefined or not. This process of writing an if statement each time feels a little clumsy and I was wondering if there is a better way to perform this check or a way to avoid it entirely.
Check the array before using map:
arr && arr.map()
OR,
arr && arr.length && arr.map() // if you want to map only if not empty array
OR,
We can even use like this (as commented by devserkan):
(arr || []).map()
As per your comment:
I wish there was a safe navigation operator like with C# (arr?.map())
Yes, obviously. This is called optional chaining in JavaScript which is still in proposal. If it is accepted, then you may use like this:
arr?.map()
You can see it in staging 1 for which you may use babel preset stage1
But obviously, except the checking array length, your requirement will not be fulfilled:
This results in an error because, of course, the first index of the array is undefined.
So, I suggest you to use:
arr && arr.length && arr.map()
What you actually need here is called optional chaining:
obj?.a?.b?.c // no error if a, b, or c don't exist or are undefined/null
The ?. is the existential operator and it allows you to access properties safely and won't throw if the property is missing. However optional chaining is not yet part of JavaScript but has been proposed and is in stage 3, see State 3 of TC39.
But, using proxies and a Maybe class, you can implement optional chaining and return a default value when the chain fails.
A wrap() function is used to wrap objects on which you want to apply optional chaining. Internally, wrap creates a Proxy around your object and manages missing values using a Maybe wrapper.
At the end of the chain, you unwrap the value by chaining getOrElse(default) with a default value which is returned when the chain is not valid:
const obj = {
a: 1,
b: {
c: [4, 1, 2]
},
c: () => 'yes'
};
console.log(wrap(obj).a.getOrElse(null)) // returns 1
console.log(wrap(obj).a.b.c.d.e.f.getOrElse(null)) // returns null
console.log(wrap(obj).b.c.getOrElse([])) // returns [4, 1, 2]
console.log(wrap(obj).b.c[0].getOrElse(null)) // returns 4
console.log(wrap(obj).b.c[100].getOrElse(-1)) // returns -1
console.log(wrap(obj).c.getOrElse(() => 'no')()) // returns 'yes'
console.log(wrap(obj).d.getOrElse(() => 'no')()) // returns 'no'
wrap(obj).noArray.getOrElse([1]).forEach(v => console.log(v)) // Shows 1
wrap(obj).b.c.getOrElse([]).forEach(v => console.log(v)) // Shows 4, 1, 2
The complete example:
class Maybe {
constructor(value) {
this.__value = value;
}
static of(value){
if (value instanceof Maybe) return value;
return new Maybe(value);
}
getOrElse(elseVal) {
return this.isNothing() ? elseVal : this.__value;
}
isNothing() {
return this.__value === null || this.__value === undefined;
}
map(fn) {
return this.isNothing()
? Maybe.of(null)
: Maybe.of(fn(this.__value));
}
}
function wrap(obj) {
function fix(object, property) {
const value = object[property];
return typeof value === 'function' ? value.bind(object) : value;
}
return new Proxy(Maybe.of(obj), {
get: function(target, property) {
if (property in target) {
return fix(target, property);
} else {
return wrap(target.map(val => fix(val, property)));
}
}
});
}
const obj = { a: 1, b: { c: [4, 1, 2] }, c: () => 'yes' };
console.log(wrap(obj).a.getOrElse(null))
console.log(wrap(obj).a.b.c.d.e.f.getOrElse(null))
console.log(wrap(obj).b.c.getOrElse([]))
console.log(wrap(obj).b.c[0].getOrElse(null))
console.log(wrap(obj).b.c[100].getOrElse(-1))
console.log(wrap(obj).c.getOrElse(() => 'no')())
console.log(wrap(obj).d.getOrElse(() => 'no')())
wrap(obj).noArray.getOrElse([1]).forEach(v => console.log(v)) // Shows 1
wrap(obj).b.c.getOrElse([]).forEach(v => console.log(v)) // Shows 4, 1, 2
I am working on Angular project and time to time I used to have check undefined or null over Object or it's properties. Normally I use lodash _.isUndefined() see example below:
this.selectedItem.filter(i => {
if(_.isUndefined(i.id)) {
this.selectedItem.pop();
}
})
I couldn't see any problem with it. But I had discussion with my colleague during review of above code. He was telling me that if i gets undefined before the if statement then it will throw the exception. Instead he suggested me to always check i or i.id like this:
if(!!i && !!i.id) {
this.selectedItem.pop();
}
I am convinced what he was trying to say unlike his way of checking undefined in above code. But then I was thinking what is the purpose of lodash _.isUndefined?
Could anyone please let me know what is the best or clean way to do it. Because for me !!i && !!i.id is not readable at all.
Many thanks in advance.
You can use _.isNil() to detect undefined or null. Since you're using Array.filter(), you want to return the results of !_.isNil(). Since i is supposed to be an object, you can use !_.isNil(i && i.id).
Note: you are using Array.filter() as Array.forEach(). The callback of Array.filter() should return a boolean, and the result of the filter is a new array.
const selectedItem = [
undefined,
{},
{ id: 5 },
undefined,
{ id: 7 },
];
const result = selectedItem.filter(i => !_.isNil(i?.id));
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
You can also use _.reject() and save the need to add !:
const selectedItem = [
undefined,
{},
{ id: 5 },
undefined,
{ id: 7 },
];
const result = _.reject(selectedItem, i => _.isNil(i?.id));
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
Use typeof i.id === 'undefined' to check for undefined and i.id === null to check for null.
You could write your own helper functions to wrap any logic like what LoDash has. The condition with !!i && !!i.id is only looking for falsy values (empty string, 0, etc), not only null or undefined.
You could check for i and if it is not truthy or if the property is undefined or null, then do something.
if (!i || i.id === undefined || i.id === null) {
this.selectedItem.pop();
}
Referring to a variable which has undefined as it's value won't throw any error. You get a ReferenceError for referring to variable that is not defined:
> i
Uncaught ReferenceError: i is not defined
If you pass a not-defined variable to a function a ReferenceError is thrown and the function won't be executed.
> _.isUndefined(i)
Uncaught ReferenceError: i is not defined
typeof operator should be used for safely checking whether a variable is defined or not:
> typeof i
'undefined'
In your code the i is defined (it's a function argument) so by referring to it you won't get any ReferenceError. The code will throw a TypeError when i is defined, has undefined value and you are treating it as an object:
> var i = undefined; i.id
Uncaught TypeError: Cannot read property 'id' of undefined
let words = [null, undefined, 'cheaters', 'pan', 'ear', 'era']
console.log(words.filter(word => word != null));
Your friend is right. When you do, _.isUndefined(i.id) you're assuming i to not to be undefined. You're assuming i is an object which will have an id property which you're checking if it is falsey or not.
What happens when the variable i itself is undefined? So you will end up undefined.id which is an error. Therefore you could simply do this
if(i && i.id) {
// You're good to go
}
The above will check for all falsey values, 0 and "" included. So if you want to be very specific, then you'll have to check the types of both using typeof operator.
May I suggest checking for if(typeof(element) == "number" && element) {} In this case the typeof() part catches any non numbers and the element part should catch any NaNs (typeof(NaN) returns "number")
You can use lodash#get (it will handle if the root is value null or undefined), and then compare the output with null using == or !=, instead of using === or !==. if the output is null or undefined then comparing with == will be true and != will be false.
const selectedItem = [
undefined,
{},
{id: null},
{ id: 5 },
undefined,
{ id: 0 },
{ id: 7 },
];
const res = selectedItem.filter(a => _.get(a, 'id') != null);
console.log(res);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
using lodash#get you can checkfor any nested level for its existence with out subsequent && like a && a.b && a.b.c && a.b.c.d (in case of d) look at this answer of mine for nested level checking with lodash#get
Also you can use _.isNill instead of comparing with null will == or !=
Another more "lodashy" approach would be to use _.conforms. The readability is much better in my opinion and you get access directly to id so no problems with undefined before that:
const items = [
undefined,
{ id: null},
{ id: 5 },
{ id: "4" },
{ id: undefined },
undefined,
{ id: 0 },
{ id: 7 },
{ id: () => 3 }
];
const numbersOnly = _.filter(items, _.conforms({'id': _.isNumber}));
console.log('numbers', numbersOnly);
const allDefined = _.filter(items, _.conforms({'id': _.negate(_.isUndefined)}));
console.log('defined', allDefined);
const stringsOnly = _.filter(items, _.conforms({'id': _.isString}));
console.log('strings', stringsOnly);
const functionsOnly = _.filter(items, _.conforms({'id': _.isFunction}));
console.log('functions', functionsOnly);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
Javascript has now (Chrome 80, Safari 13.4) Optional chaining (?.)
The optional chaining operator (?.) permits reading the value of a
property located deep within a chain of connected objects without
having to expressly validate that each reference in the chain is
valid.
This means you could check for id without causing an exception in case i is undefined
this.selectedItem.filter(i => {
if(i?.id) {
this.selectedItem.pop();
}
})
Or since you are using filter, you can check test this live on the filter documentation.
const words = ['spray', undefined, 'elite', 'exuberant', 'destruction', 'present'];
const result = words.filter(word => word?.length > 6);
console.log(result);
// expected output: Array ["exuberant", "destruction", "present"]
Additionally, also worth to mention, as #Koushik Chatterjee's answer points, lodash _.get allows you to describe a path to a deep property safely, and even give a default value in case it doesn't exist.
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.get(object, ['a', '0', 'b', 'c']);
// => 3
_.get(object, 'a.b.c', 'default');
// => 'default'