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
I am tring to filter for a specific type of object from a list of union types. Here is my attempt so far.
/* #flow */
type Human = {|
name: string
|};
type Droid = {|
model: string
|};
type LivingThings = {
things: [Human | Droid]
}
const getHumans = (livingThings: LivingThings): Human[] => {
return livingThings.things.filter((thing) => {
return 'name' in thing;
})
}
However this complains of an error as per this link: Link
return livingThings.things.filter((thing) => {
^ Cannot return livingThings.things.filter(...) because property model is missing in Human 1 but exists in Droid [2] in arr
Unfortunately, Flow does not refine types with filter. The linked issue is quite old and it has been on the roadmap for quite some time. As suggested above you can cast to any or use $ExpectError to create a locally unsafe function. Alternatively, you can use an imperative loop approach:
const res: Human[] = [];
for (thing of livingThings.things) {
if (typeof thing.name === 'string') {
res.push(thing)
}
}
return res;
Or create a useful helper function mapMaybe that you can reuse in many other cases:
function mapMaybe<A, B>(f: A => ?B, xs: A[]): B[] {
// $ExpectError or alternatively write loop here
return xs.map(f).filter(notUndefinedOrNull);
}
mapMaybe(thing => {
if (typeof thing.name === 'string') {
return thing;
}
return null;
}, livingThings.things);
I'm attempting to basically write a custom ORM for a small project and I am using a class to set up my records, by default all records will pass through a sort of type checker that will convert any values based on what I said they should be
export default class Record {
constructor(...props) {
const [model, data] = props;
forOwn(data, (value, key) => {
if (model.hasOwnProperty(key)) {
this[key] = attr(model[key], value);
} else {
this[key] = value;
}
});
}
}
This works just fine by looping over an array of data and creating new instances from it
data.forEach((item) => {
serializedData.push(new Item(model, item));
});
Where Item is just a simple extended class
class Item extends Record {
constructor(...props) { super(...props); }
}
Here is where I'm having a problem, I want to add a new itemImage property to the Item subclass that will take some existing values and create a URL from them.
I've tried both of these that I've found from searching this on the net and neither works
Reflect.defineProperty(Item.prototype, 'itemImage', {
get() {
return `//res.cloudinary.com/***/image/upload/${this.image.image_crop}/${this.image.image_version}/${this.auction_code}/${this.image.original_image_name}`;
}
});
And
class Item extends Record {
...
get itemImage() {
return this.getItemImage();
}
getItemImage() {
return `//res.cloudinary.com/***/image/upload/${this.image.image_crop}/${this.image.image_version}/${this.auction_code}/${this.image.original_image_name}`;
}
}
How would I go about doing this?
EDIT
Here is what attr does
const attr = (type, data) => {
switch (type) {
case 'number':
return parseFloat(data);
case 'string':
return data.toString();
case 'object':
if (typeof data === 'string' && data.indexOf('{') > -1) {
return JSON.parse(data);
} else {
return data;
}
}
};
Here is a full Sandbox with react to demonstrate
https://codesandbox.io/s/7m388jwk1q
Your problem is not related to subclass getters. The issue is in your data format.
Take a look at the image property:
"image": "[{\"original_image_name\":\"iqsnzsutgbpgqsztani5.png\",\"image_crop\":\"c_crop,h_450,w_1125\"}]"
When it is parsed by attr function, it becomes an array, not an object. So you can't access fields like this: ${this.image.image_crop}.
But everything works fine if you remove the square brackets. Or, if you can't change the data format, just access the image like this: ${this.image[0].image_crop}
I have a redux store. To change the data in the store, the typical way is to create an action, actionCreator, a reducer and then dispatch the action.
For a small to medium sized app, it looks like an overkill to change at so many place to reflect such changes. So I created a generic reducer which looks something like this :
// here state is a copy of actual state, so I can mutate it directly
const reducer = ( state, action) => {
if(action.type == 'SETTER'){
try{
return assign(state, action.data.target, action.data.value )
}
catch(err){
console.log('WARNING: the key wasn\'t valid', err)
}
}
return state;
}
this assign method goes like this:
const assign = (obj, prop, value) => {
if (typeof prop === "string")
prop = prop.split(".");
if (prop.length > 1) {
var e = prop.shift();
assign(obj[e] , prop, value);
} else
obj[prop[0]] = value;
return obj
}
Then I have a a generic action dispatcher and a container component, which allow me to do something like this :
containerComponent.set( 'user.name', 'Roy' )
containerComponent.set( 'order.receiver.address', 'N/A')
The action which fires when set is called on the containerComponent looks like this :
{
type : 'SETTER',
data : {
target : 'user.name',
value : 'Roy'
}
}
As you can see, this generic reducer allows me to never write a reducer again, but I am still dispatching an action whenever state changes, so no violation of any of the core principles of redux.
Are there any minor/major shortcomings in this approach, especially in terms of performance? And also, where do you find this approach to be useful.
As you noted quite right, Redux requires you to implement multiple layers of indirection between the point where something in your application happens, and the point where the store is actually updated to reflect this event.
This is by design.
Global state generally creates the problem that it can be changed arbitrarily from anywhere in your application, without a simple way of understanding how or why. And yes, a Redux store is effectively global state.
By separating the questions of what happened (as represented by an action and described by the action's payload) from how does that affect the global state (as defined by the reducer), Redux removes this issue to a certain degree. Instead of allowing arbitrary changes to the global state, only certain, very specific combinations of changes can be made, triggered by well-defined events that in the best case come with enough semantic information attached to them to enable tracing them back to their origin.
By undermining this core idea of Redux by creating a single pair of generic action and reducer, you loose one of the core advantages of Redux, and are left with a set of indirections and abstractions between your components and the store that don't really bring you any significant benefits.
It is common wisdom that code that doesn't create value is code best deleted. In my mind, you may be much better off not using Redux and simply using component state instead rather than using a crippled implementation of Redux.
An interesting read regarding this topic from Dan Abramov, who created Redux: You might not need Redux.
Timo's answer does a good job explaining why your implementation sort of goes against a lot of the principles of Redux. I would just add that you might find Mobx interesting. I think it more closely resembles the sort of state management you're trying to get.
This function (assign called by reducer) is not according Redux rules, this is not immutable 'pure' function because mutate state.
Test:
const assign = (obj, prop, value) => {
if (typeof prop === "string")
prop = prop.split(".");
if (prop.length > 1) {
var e = prop.shift();
assign(obj[e], prop, value);
} else
obj[prop[0]] = value;
return obj
}
const reducer = (state, action) => {
if (action.type == 'SETTER') {
try {
return assign(state, action.data.target, action.data.value)
}
catch (err) {
console.log('WARNING: the key wasn\'t valid', err)
}
}
return state;
}
const testReducer = () => {
const user = {
id: 0,
name: ''
};
const action = {
type: 'SETTER',
data: {
target: 'name',
value: 'Roy'
}
};
console.log('user before: ', user);
reducer(user, action);
console.log('user after: ', user);
};
testReducer();
Test results:
user before: { id: 0, name: '' }
user after: { id: 0, name: 'Roy' }
Easiest fix:
const assign = (obj, prop, value) => {
var tempObj = Object.assign({}, obj);
if (typeof prop === "string")
prop = prop.split(".");
if (prop.length > 1) {
var e = prop.shift();
assign(tempObj[e], prop, value);
} else
tempObj[prop[0]] = value;
return tempObj
}
EDIT
Fix without copy the values of the state object to temp target object:
const assign = (obj, prop, value) => {
if (typeof prop === "string")
prop = prop.split(".");
if (prop.length > 1) {
var e = prop.shift();
assign(obj[e], prop, value);
} else {
return {
...obj,
[prop[0]]: value
};
}
}
Reducer are now simple functions and can be easily reuse
const getData = (state, action) => {
return {...state, data: state.data.concat(action.payload)};
};
const removeLast = (state) => {
return {...state, data: state.data.filter(x=>x !== state.data[state.data.length-1])};
}
Action type and reducer function are now declared in an array
const actions = [
{type: 'GET_DATA', reducer: getData},
{type: 'REMOVE_LAST', reducer: removeLast},
{type: 'REMOVE_FIRST', reducer: removeFirst},
{type: 'REMOVE_ALL', reducer: removeAll},
{type: 'REMOVE_BY_INDEX', reducer: removeByIndex}
];
Initial state for the reducer
const initialState = {
data: []
}
actionGenerators creates an unique Id using Symbol and assign that Id to actions and reducer function.
const actionGenerators = (actions) => {
return actions.reduce((a,c)=>{
const id = Symbol(c.type);
a.actions = {...a.actions, [c.type]: id};
a.reducer = a.reducer ? a.reducer.concat({id, reducer: c.reducer}) : [{id, reducer: c.reducer}];
return a;
},{});
}
reducerGenerators is a generic reducer creator.
const reducerGenerators = (initialState, reducer) => {
return (state = initialState, action) => {
const found = reducer.find(x=>x.id === action.type);
return found ? found.reducer(state, action) : state;
}
}
Usage
const actionsReducerCreator = actionGenerators(actions);
const store = createStore(reducerGenerators(initialState, actionsReducerCreator.reducer));
const {GET_DATA} = actionsReducerCreator.actions;
store.dispatch({type: GET_DATA});
I have implemented this in my todo application on my github
Redux-Reducer-Generator
Using flowtype on a current react / redux project.
I define in my actions.js file a disjoint union type:
export type ArticleAction =
{ type: 'ARTICLE_SET_EDITION' }
| { type: 'ARTICLE_BLABLA', blip: string };
And then in my reducer I have
import type { ArticleAction } from './actions';
[...]
const articlesReducer = (state: any = initialState, action: ArticleAction): any => {
if (action.type === 'ARTICLE_BLABLA') {
const test = action.blip.shoups;
return test;
}
}
Flow does not detect a problem.
But! if I declare ArticleAction directly in reducer.js, it does recognize that action.blip.shoups is invalid because blip is a string.
Any idea about what I am doing wrong ?
thx
TL;DR Flow doesn't error in situations like this today, but most likely will in the future.
This doesn't have anything to do with the import/exports or even the union type, you could simplify it all the way down to this:
function method(val: 'foo') {
if (val === 'bar') {
// unreachable...
}
}
Flow can see that it is a impossible refinement and could know that the inner code is unreachable. However, Flow does not error in unreachable scenarios. Today it simply marks the value of val as an "empty" type in that code path and moves on.
We have started to lay the groundwork for this reachability analysis and will use it to create errors in future versions of Flow.
We can also use reachability analysis to test exhaustiveness, i.e.:
function method(val: 'foo' | 'bar') {
if (val === 'foo') {
// ...
} else if (val === 'bar') {
// ...
} else {
// possibilities of val have been exhausted, this is unreachable...
}
}
These are common requests and we are working on them.