Issue in Typing a class with Flowjs - javascript

I have the following code that I'm attempting to type with Flow
type Metadata = {
currentPage: string,
};
type State = {
result: {
metadata: Metadata,
}
}
type EmptyObject = {};
type Test = Metadata | EmptyObject;
class HelperFn {
state: State;
metadata: Test;
constructor(state: State) {
this.state = state;
if (state && state.result && state.result.metadata) {
this.metadata = state.result.metadata;
} else {
this.metadata = {};
}
}
getCurrentPageNumber() {
return this.metadata.currentPage;
}
}
I've created Types that I'm assigning later on. In my class, I assign the type Test to metadata. Metadata can be either an object with properties or an empty object. When declaring the function getCurrentPageNumber, the linter Flow tells me that it
cannot get 'this.metadata.currentPage' because property 'currentPage' is missing in EmptyObject
Looks like Flow only refers to the emptyObject. What is the correct syntax to tell Flow that my object can either be with properties or just empty?

Since metaData can be empty, Flow is correctly telling you that this.metadata.currentPage may not exist. You could wrap it in some sort of check like
if (this.metadata.currentPage) {
return this.metadata.currentPage
} else {
return 0;
}
To get it to work properly.

Related

Specify values of a parameter type in Typescript

I have these interfaces:
interface IComponent {
type: string;
text: string;
}
interface IComponents {
cc: IComponent;
lajota: IComponent;
}
interface IMain {
components: IComponents
}
And it is working fine! But now I need to add a new component called "caneta".
SO I'll access this with .components.caneta. But this new component will have a single attribute:
interface IComponentCaneta {
property: string;
}
// Add the new component to be used on IMain
interface IComponents {
cc?: IComponent;
lajota?: IComponent;
caneta?: IComponentCaneta;
}
The problem is I have a method that do some work depending on the attribute type like:
//for each component I have in components objects
function myFunc(component: IComponent) {
_.each(components (val, key) => {
if (key === 'cc') {...}
else if (value?.type === 'xxx') { <---- HERE flags error
components[key].type = 'xxxx'
}
})
}
When I add the new component caneta, Typescript complains saying:
Property 'type' does not exist on type 'IComponentCaneta'.
Tried to make type optional, but didn't work.
What would be the right thing to do in this situation?
Is there a way to explicitly say that "The attribute of type IComponent will be 'X' 'Y' or 'Z'. Something like
function myFunc(component: IComponent ['cc' or 'lajota'])
Things I tried and failed:
// make type optional
interface IComponent {
type?: string;
text: string;
}
// try to infer the object (cc, loja, caneta)
switch (type) {
case 'cc':
// ...
break;
case 'lajota':
// ...
break;
default: //'caneta'
// ...
break;
}
//using IF/ELSE
if (type === 'cc') {.../}
else if(type === 'lajota') {...}
else if(type === 'caneta') {...}
I found a solution using Object Entries and forEach.
I don't know if there is a "down side" yet. I just wanted a way to iterate through the components and make typescript happy.
The only solution I could think of was try to infer the object so TS could "see" the right attributes.
function myFunc (components: IComponents) {
Object.entries(components).forEach(([key, value], index) => {
if (key === 'caneta') {
components[key].property = 'Hello World';
} else if(value?.type === 'text') { <--- No longer gives me errors
components[key].type = 'NEW TYPE'
}
});
}
One thing that kind worries me is that when I was trying this code on Typescript Playground it gave me the following error/warning:
Object is possibly 'undefined'.
on the following lines:
components[key].property = 'Hello World';
and
components[key].type = 'NEW TYPE'
No errors on my vscode/lint though

Imbricated json object value change

I have an empty javascript object, and I want to do something like this:
this.conflictDetails[this.instanceId][this.modelName][this.entityId] = { solved: true };
The problem is that for the modelName I get Cannot read properties of undefined.
I think it is because the empty object this.conflictDetails does not have the property that I m looking for, I wonder how I could write this to work and to be as clear as possible?
The result should look like:
{
'someInstanceId': {
'someModelName': {
'some entityId': {
solved: true;
}
}
}
}
With mu; time instance ids, that hold multiple model names, that have multiple entity Ids
check this the '?' operator
let obj = {
'someInstanceId': {
'someModelName': {
'someEntityId': {
solved: true
}
}
}
};
let value1 = obj.someInstanceId?.someModelName?.someEntityId;
console.log(value1);
// or
value1 = obj["someInstanceId"] ?? {};
value1 = value1["someModelName"] ?? {};
value1 = value1["someEntityId"];
console.log(value1);
let value2 = obj.someInstanceId?.someMissModelName?.someEntityId;
console.log(value2);
You can use interfaces to define the model of the object, or you can check if every nested object is not null nor undefined.
I would prefer the first option if I use typescript. If not, then i would stick to the not null/undefined option
First we create a model:
export interface CustomObject {
[instanceId: string]: {
[modelName: string]: {
[entityId: string]: {
solved: boolean;
};
};
};
}
Now that we have a model we can fill an object with data:
We define the object in the .ts file of your component
public dataObjects: CustomObject = {
instance1: {
basicModel: {
Entity1: {
solved: false,
},
},
}
};
Now you can have for example a function to change the solved status, I assume that there are several instanceIds, modelNames and entityIds.
public resolveObject(
instanceId: string,
modelName: string,
entity: string,
solved: boolean
) {
this.dataObjects[instanceId][modelName][entity].solved = solved;
}
Now you can call the method with the object details:
this.resolveObject('instance1', 'basicModel', 'Entity1', true);
After this line is executed the solved will changer from false to true.
I've created a StackBlitz to show you: https://stackblitz.com/edit/angular-ivy-ibfohk

Different outcomes between Object assign and Object spread

I am using typescript and compiling to ES2016.
I noticed that I am calling two different functions with similar this object in a Function.prototype.call().
I tried to merge both this objects by using a common object which would use ...spread in the beginning of the object like so
let selfShared = {
props,
// ...
};
let selfHost = {
...selfShared,
// ...
};
let selfGuest = {
...selfShared,
// ...
};
The idea of using the spread in the begging was that I could overwrite the shared properties in either of the this objects if I saw it fit.
But unlike when setting props straight in the this objects using the spread gave out weird results, which turned out to be because tsc compiled the code as
let selfShared = {
props
};
let selfHost = Object.assign(Object.assign({}, selfShared), {
// ...
});
// ...
using my code
let state = undefined;
let attributes = {};
let selfShared = {
props: attributes
};
let selfHost = {
...selfHost,
get state() {
console.log("selfHost get state");
return state;
},
set state(originalStates) {
console.log("selfHost set state");
!state ? state = originalStates : console.error("`this.states` already defined in the host function.");
}
}
the output looks like
let state = undefined;
let attributes = {};
let selfShared = {
props: attributes
};
let selfHost = Object.assign(
Object.assign({}, selfShared), {
get state() {
console.log("selfHost get state");
return state;
},
set state(originalStates) {
console.log("selfHost set state");
!state ? state = originalStates : console.error("`this.states` already defined in the host function.");
}
});
now at least on firefox 74 to 77 inserting both of the codes into the console and adding
// ...
selfHost.state = {
thing: "some"
};
selfHost.state = {
some: "thing"
};
throws out different logs...
The precompiled code gives me two of set state and an error which are the expected outputs, but the compiled code gives me a get state and ignores the rule in set state outputting
{
some: "thing"
}
instead of the expected
{
thing: "some"
}
as in the precompiled code?
Setting the spread into the bottom of the file compiles to
let selfHost = Object.assign({
get state() {
console.log("selfHost get state");
return state;
},
set state(originalStates) {
console.log("selfHost set state");
!state ? state = originalStates : console.error("`this.states` already defined in the host function.");
}
}, selfShared);
which gives the right output but doesn't allow me to overwrite the properties given by selfShared.
Can you explain why this happens with Object.assign and if there is a trick to get an output from tsc that still lets me do what I originally wanted?
When using spread
let obj = {
...otherObj,
// ...
}
or
let obj = Object.assign({}, otherObj, {
// ...
})
The spread is interpreted as literal which means that as in the polyfill the properties are read as strict values, which means that normal values are read normally, setters are ignored and getters are read as normal values.
Setters and getters work as written in the question when the spread is written in the end as
let obj = {
// ...
...otherObj
}
or
let obj = Object.assign({
// ...
}, otherObj)
since otherObj only extends the unique objects.

Javascript, in a React application assign to {} in a function component, code review

I have this code in a friend of mine React application and I need to understand what this code does explicitly:
const Component = ()=> (
<QueryFetcher>
{({ data }) => {
const { user: { profile = {} } = {} } = data
return (
<div>
{profile.username && profile.username}
</div>
)
}}
</QueryFetcher>
)
What is this line for?
const { user: { profile = {} } = {} } = data
Is it correct to assign something to {} using { user: { profile = {} } = {} } in this functional component? Or in a render() hook of a stateful component in React?
const { user: { profile = {} } = {} } = data basically means that your retrieving the user profile.
const means that you are creating a new variable
{ user: { profile } } } means that you are retrieving profile inside of user
= {} means that if the object is undefined, use an empty object so it will not fail because doing user.profile will throw an error if user is undefined.
= data means that you retrieving this info from the data variable
So, this line means, from the variable data, go take the user, if the user is undefined, use an empty object. Then, go take the profile, if the profile is undefined, use an empty object. Then create a variable called profile with the result. This is like doing this:
const user = data.user === undefined ? {} : data.user;
const profile = user.profile === undefined ? {} : user.profile;
What is this line for?
const { user: { profile = {} } = {} } = data
It's basically just chained ES6 object-destructuring with default values.
What this line does in words:
Read "user" from "data", if "user" is undefined, assign {} as a default value
Read "profile" from "user", if "profile" is undefined, assign {} as a default value
Is it correct
It is mostly a short-hand syntax used to remove repetitive stuff. So instead of accessing multiple object props separately e.g.
this.props.prop1, this.props.prop2, ...
you can use
const { prop1, prop2 } = this.props;
It also helps other readers later quickly understanding what variables are used in a method if all necessary props are destructured at the start.

React to nested state change in Angular and NgRx

Please consider the example below
// Example state
let exampleState = {
counter: 0;
modules: {
authentication: Object,
geotools: Object
};
};
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>){
this.counter = store.select('counter');
}
}
Here in the MyAppComponent we react on changes that occur to the counter property of the state. But what if we want to react on nested properties of the state, for example modules.geotools? Seems like there should be a possibility to call a store.select('modules.geotools'), as putting everything on the first level of the global state seems not to be good for overall state structure.
Update
The answer by #cartant is surely correct, but the NgRx version that is used in the Angular 5 requires a little bit different way of state querying. The idea is that we can not just provide the key to the store.select() call, we need to provide a function that returns the specific state branch. Let us call it the stateGetter and write it to accept any number of arguments (i.e. depth of querying).
// The stateGetter implementation
const getUnderlyingProperty = (currentStateLevel, properties: Array<any>) => {
if (properties.length === 0) {
throw 'Unable to get the underlying property';
} else if (properties.length === 1) {
const key = properties.shift();
return currentStateLevel[key];
} else {
const key = properties.shift();
return getUnderlyingProperty(currentStateLevel[key], properties);
}
}
export const stateGetter = (...args) => {
return (state: AppState) => {
let argsCopy = args.slice();
return getUnderlyingProperty(state['state'], argsCopy);
};
};
// Using the stateGetter
...
store.select(storeGetter('root', 'bigbranch', 'mediumbranch', 'smallbranch', 'leaf')).subscribe(data => {});
...
select takes nested keys as separate strings, so your select call should be:
store.select('modules', 'geotools')

Categories

Resources