Flow not detecting if-block handling potentially undefined (maybe) value. Why? - javascript

I am seeing a behavior in Flow that I don't understand. The function that I'm trying to type is a redux reducer. The problem that I'm seeing seems to stem from the fact that the action object provided to the reducer has a payload that is marked with the "maybe" operator: ?. That type looks like this essentially:
type Action = {
type: 'foo',
payload?: { ... }
}
In the reducer I tried to provide an if-gate to handle the case when payload is not defined. In theory, it seems this should work. The simplest case works (see here):
type Foo = {
type: 'foo',
payload?: {
foo: 'foo'
}
};
type Bar = {
type: 'bar',
payload?: {
bar: 'bar',
}
};
const reducer = (state: {} = {}, action: Foo | Bar) => {
switch (action.type) {
case 'foo': {
if (!action.payload) {
return state;
}
return action.payload.foo;
}
case 'bar': {
if (!action.payload) {
return state;
}
return action.payload.bar;
}
default:
return state;
}
}
However, in my actual reducer, which is a bit more involved, I'm unable to do away with the errors. My reducer looks something like this:
const camelize = (x) => x;
const getSearchTypeFromPath = (x) => x;
type A = {
type: '##router/LOCATION_CHANGE',
payload?: {
pathname: string,
query: {},
}
}
type B = {
type: 'action.foo',
payload?: {
mode: string,
params: {}
}
}
const byMode = (
state: {} = {},
action: A | B,
) => {
switch (action.type) {
case '##router/LOCATION_CHANGE': {
if (!action.payload) {
return state;
}
const modeKey: string = camelize(getSearchTypeFromPath(action.payload.pathname));
return {
...state,
[modeKey]: action.payload.query,
};
}
case 'action.foo': {
if (!action.payload) {
return state;
}
const modeKey: string = camelize(action.payload.mode);
return {
...state,
[modeKey]: action.payload.params,
}
}
default:
return state;
}
};
This code, unlike the simplified case above, produces some flow errors: see here:
33: [modeKey]: action.payload.query,
^ Cannot get `action.payload.query` because property `query` is missing in undefined [1].
References:
6: payload?: { ^ [1]
44: [modeKey]: action.payload.params,
^ Cannot get `action.payload.params` because property `params` is missing in undefined [1].
References:
14: payload?: { ^ [1]
In terms of the logic gate handling the possibly undefined payload they are I think the same. So why the error? It's also interesting that there is no error for the first properties that are referenced in each case block: i.e. pathname and mode. Finally, one other thing I noticed is that if I remove the helper functions, which in my example here have just been turned into dummy functions, then I don't get the errors. See here.
Is there anyone out there that can explain what's going on here. I can't make sense of it yet.

The issue is the call to camelize.
Flow resets all type refinements after a function call where that function call could potentially mutate a value. In your example, action is a parameter, so it's plausible that something like this happens:
function camelize(x) {
someAction.payload = undefined;
}
// Now your code crashes
byMode({}, someAction);
Generally the solution is to extract out the thing being tested into a const:
const payload = action.payload;
if (payload === undefined) return;
// OK to use 'payload' after function calls now
See also https://flow.org/en/docs/lang/refinements/#toc-refinement-invalidations or e.g. https://github.com/facebook/flow/issues/5393

type A = {
type: '##router/LOCATION_CHANGE',
payload: {
pathname: string,
query: {},
}
}
type B = {
type: 'action.foo',
payload: {
mode: string,
params: {}
}
}
Don't know the purpose of ? but by removing this will remove your errors.

The only way I have been able to get this to work is to add some ternary checks for each of the references to those properties even after the logic gate like this:
const camelize = (x) => x;
const getSearchTypeFromPath = (x) => x;
type A = {
type: '##router/LOCATION_CHANGE',
payload?: {
pathname: string,
query: {},
}
}
type B = {
type: 'action.foo',
payload?: {
mode: string,
params: {}
}
}
const byMode = (
state: {} = {},
action: A | B,
) => {
switch (action.type) {
case '##router/LOCATION_CHANGE': {
if (!action.payload) {
throw new Error('Ooops');
}
const modeKey: string = camelize(getSearchTypeFromPath(action.payload ? action.payload.pathname : ''));
return {
...state,
[modeKey]: action.payload ? action.payload.query: {},
};
}
case 'action.foo': {
if (!action.payload) {
return state;
}
const modeKey: string = camelize(action.payload.mode || '');
return {
...state,
[modeKey]: action.payload ? action.payload.params : {},
}
}
default:
return state;
}
};
See here for example.
I don't understand why this works even though the code in question wouldn't be reached if action.payload is undefined. The code required to please flow in this case seems less than ideal as it requires checks that don't seem necessary.

Related

Ngrx store dispatch is not accepting string as arguments

I'm taking a test in which I should write code a in such a way that all unit test case gets passed.
Case 1:
it('should dispatch action when dispatchAction is called', async() => {
// you need to spy on store's 'dispatch' method
store = TestBed.get(Store);
spyOn(store, 'dispatch').and.callThrough();
// if you call function dispatchAction with 'movies' paramter. expect store to dispatch action='movies'
component.dispatchAction('movies');
fixture.detectChanges();
expect(store.dispatch).toHaveBeenCalledWith('movies');
});
My code:
dispatchAction($event: string) {
this.store.dispatch({type: 'movie'});
}
But the spec is getting failed throwing the below error
Expected spy dispatch to have been called with [ 'movies' ] but actual calls were [ Object({ type: 'movies' }) ].
Reducer,
export function news (state = initialState, action: Action) {
switch (action.type) {
case LOAD_SECTION_NEWS: {
return {
newsList: mockNewsList,
filter: action.type
};
}
case FILTER_SUBSECTION: {
return {
newsList: mockNewsList,
filter: action.payload
};
}
default:
return state;
}
}
export const getNewsList = (state: any) => {
return state;
};
export const getFilter = (state: any) => {
return state;
};
Action
export class NewsActions {
static LOAD_SECTION_NEWS = '[News] LOAD_SECTION_NEWS';
static FILTER_SUBSECTION = '[News] FILTER_SUBSECTION';
LoadSectionNews(list: News[]): Action {
return {
type: '',
payload: ''
};
}
FilterSubsection(subsection: string) {
return {
type: '',
payload: ''
};
}
}
How do I modify, the reducer in such a way that the unit test case get passed.
This Ngrx is out of syllabus and I've no idea. Please help.
The error reported is about .toHaveBeenCalledWith('movies'); from your test case. The expectation is the word movies to have been used as argument, and this is incorrect.
When you call this.store.dispatch({type: 'movies'}); in your controller, it is passing the object {type: 'movies'} as argument.
as your test is expecting only the word movie, it fails
change your expectation to
expect(store.dispatch).toHaveBeenCalledWith({type: 'movies'});
that will fix your test
Good luck with your studies
var data = 'movies';
this.store.dispatch(data as any)
var data = 'movies';
this.store.dispatch(data as any)
You can achieve the result by casting the string to any

Flow vs TypeScript action creator (Redux)

I'm trying to figure out how to implement type annotation for Redux with Flow.
With TypeScript:
const PLAY = 'PLAY';
const RUN = 'RUN';
class PlayAction {
readonly type = PLAY;
constructor(public payload: string) {}
}
class RunAction {
readonly type = RUN;
constructor(public payload: boolean) {}
}
type Actions = PlayAction | RunAction;
function dummyReducer(state: any, action: Actions) {
switch(action.type) {
case PLAY: {
const typeTest = action.payload; // type: string
break;
}
case RUN: {
const typetest = action.payload; // type: boolean
break;
}
}
}
In flow documentation, I found this example: Redux with Flow.
// #flow
type State = { +value: boolean };
type FooAction = { type: "FOO", foo: boolean };
type BarAction = { type: "BAR", bar: boolean };
type Action = FooAction | BarAction;
function reducer(state: State, action: Action): State {
switch (action.type) {
case "FOO": return { ...state, value: action.foo };
case "BAR": return { ...state, value: action.bar };
default:
(action: empty);
return state;
}
}
The big problems with Flow approach:
1) use in 2 places strings, so it bad practice for miss spelling mistake.
2) is not maintainable to change the string in 2 places every time I want to change the action type value.
How can I solve this problem with Flow? Any ideas?
I found the some answer, but it don't work pretty much,
and again you can't work with class action creators. :/
type _ExtractReturn<B, F: (...args: any[]) => B> = B;
export type ExtractReturn<F> = _ExtractReturn<*, F>;
Action file //
export const SET_NAME = 'SET_NAME';
export const SET_AGE = 'SET_AGE';
const setName = (name: string) => {
return {type: SET_NAME, payload: name}
}
const setAge = (age: number) => {
return {type: SET_AGE, payload: age}
}
export type Actions =
ExtractReturn<typeof setName> |
ExtractReturn<typeof setAge>
credit: Shane Osbourne
Redux & Flow-type

How to represent algebraic data types and pattern matching in JavaScript

In functional language like OCaml, we have pattern matching. For example, I want to log users' actions on my website. An action could be 1) visiting a web page, 2) deleting an item, 3) checking the profile of another user, etc. In OCaml, we can write something as follows:
type Action =
| VisitPage of string (* www.myweb.com/help *)
| DeletePost of int (* an integer post id *)
| ViewUser of string (* a username *)
However, I am not sure how to define this Action in JavaScript. One way I could imagine is
var action_1 = { pageVisited: "www.myweb.com/help", postDeleted: null, userViewed: null }
var action_2 = { pageVisited: null, postDeleted: 12345, userViewed: null }
var action_3 = { pageVisited: null, postDeleted: null, userViewed: "SoftTimur" }
But this structure does not express that pageVisited, postDeleted and userViewed are exclusive among them.
Could anyone propose a better representation of this type in JavaScript?
Is there a common way to do pattern matching in JavaScript or TypeScript?
You want a discriminated union, which TypeScript supports by adding a common property with different string literal values, like so:
type VisitPage = { type: 'VisitPage', pageVisited: string }
type DeletePost = { type: 'DeletePost', postDeleted: number }
type ViewUser = { type: 'ViewUser', userViewed: string }
type Action = VisitPage | DeletePost | ViewUser
The Action type is discriminated by the type property, and TypeScript will automatically perform control flow analysis to narrow an Action when you inspect its type property. This is how you get pattern matching:
function doSomething(action: Action) {
switch (action.type) {
case 'VisitPage':
// action is narrowed to VisitPage
console.log(action.pageVisited); //okay
break;
case 'DeletePost':
// action is narrowed to DeletePost
console.log(action.postDeleted); //okay
break;
case 'ViewUser':
// action is narrowed to ViewUser
console.log(action.userViewed); //okay
break;
default:
// action is narrowed to never (bottom),
// or the following line will error
const exhausivenessWitness: never = action; //okay
throw new Error('not exhaustive');
}
}
Note that you can add an exhaustiveness check, if you wish, so if you ever add another type to the Action union, code like the above will give you a compile-time warning.
Hope that helps; good luck!
A type in functional programming can be mimicked with a class:
class Action {}
class VisitPage extends Action {
constructor(pageUrl){
super();
this.pageUrl = pageUrl;
}
}
class ViewUser extends Action {
constructor(userName){
super();
this.userName = userName;
}
}
var myAction = new VisitPage("http://www.google.com");
console.log(myAction instanceof Action);
console.log(myAction.pageUrl);
For pattern matching:
class Action {}
class VisitPage extends Action {
constructor(pageUrl){
super();
this.pageUrl = pageUrl;
}
}
class ViewUser extends Action {
constructor(userName){
super();
this.userName = userName;
}
}
function computeStuff(action){
switch(action.constructor){
case VisitPage:
console.log(action.pageUrl); break;
case ViewUser:
console.log(action.userName); break;
default:
throw new TypeError("Wrong type");
}
}
var action = new ViewUser("user_name");
var result = computeStuff(action);
Visitor Pattern
The object-oriented incarnation of pattern matching is the visitor pattern. I've used "match", instead of "visit" in the following snippet to emphasize the correspondence.
// OCaml: `let action1 = VisitPage "www.myweb.com/help"`
const action1 = {
match: function (matcher) {
matcher.visitPage('www.myweb.com/help');
}
};
// OCaml: `let action2 = DeletePost 12345`
const action2 = {
match: function (matcher) {
matcher.deletePost(12345);
}
};
// OCaml: `let action2 = ViewUser SoftTimur`
const action3 = {
match: function (matcher) {
matcher.viewUser('SoftTimur');
}
};
// These correspond to a `match ... with` construct in OCaml.
const consoleMatcher = {
visitPage: function (url) {
console.log(url);
},
deletePost: function (id) {
console.log(id);
},
viewUser: function (username) {
console.log(username);
}
};
action1.match(consoleMatcher);
action2.match(consoleMatcher);
action3.match(consoleMatcher);
After some refactoring, you can obtain something like this, which looks pretty close to what OCaml offers:
function Variant(name) {
return function (...args) {
return { match(matcher) { return matcher[name](...args); } };
};
}
const Action = {
VisitPage: Variant('VisitPage'),
DeletePost: Variant('DeletePost'),
ViewUser: Variant('ViewUser'),
};
const action1 = Action.VisitPage('www.myweb.com/help');
const action2 = Action.DeletePost(12345);
const action3 = Action.ViewUser('SoftTimur');
const consoleMatcher = {
VisitPage(url) { console.log(url) },
DeletePost(id) { console.log(id) },
ViewUser(username) { console.log(username) },
};
action1.match(consoleMatcher);
action2.match(consoleMatcher);
action3.match(consoleMatcher);
Or
action1.match({
VisitPage(url) { console.log(url) },
DeletePost(id) { console.log(id) },
ViewUser(username) { console.log(username) },
});
Or even (using ES2015 anonymous classes):
action1.match(class {
static VisitPage(url) { console.log(url) }
static DeletePost(id) { console.log(id) }
static ViewUser(username) { console.log(username) }
});
The advantage over OCaml is that the match block is first class, just like functions. You can store it in variables, pass it to functions and return it from functions.
To eliminate the code duplication in variant names, we can devise a helper:
function Variants(...names) {
const variant = (name) => (...args) => ({
match(matcher) { return matcher[name](...args) }
});
const variants = names.map(name => ({ [name]: variant(name) }));
return Object.assign({}, ...variants);
}
const Action = Variants('VisitPage', 'DeletePost', 'ViewUser');
const action1 = Action.VisitPage('www.myweb.com/help');
action1.match({
VisitPage(url) { console.log(url) },
DeletePost(id) { console.log(id) },
ViewUser(username) { console.log(username) },
});
Since they are orthogonal, they don't have to share any structure.
If you still like the concept of "common structure" you can use class as #Derek 朕會功夫 mentioned, or use some common structure such as https://github.com/acdlite/flux-standard-action
const visitPage = { type: 'visit_page', payload: 'www.myweb.com/help' }
const deletePose = { type: 'delete_post', payload: 12345 }
const viewUser = { type: 'view_user', payload: 'SoftTimur' }

Flow union type refinement defeated by filter

The example can be found # flowtype.org/try. Here I would expect the type refinement in the conditional to work in both examples, while it only works in the simpler one. When I introduce Array.filter the refinement does not take effect. Is this a bug in Flow or mis-usage on my part?
/* #flow */
export type Action =
{| type: 'ACTION1', payload: string |}
| {| type: 'ACTION2', payload: number |}
| {| type: 'ACTION3' |}
const things = (state: Array<number> = [], action: Action): Array<number> => {
if (action.type === 'ACTION2') {
return state.filter((thing) => { return thing !== action.payload })
} else {
return state
}
}
things([1, 5], { type: 'ACTION2', payload: 5 })
const add = (state: number = 0, action: Action): number => {
if (action.type === 'ACTION2') {
return state + action.payload
} else {
return state
}
}
add(0, { type: 'ACTION2', payload: 5 })
generates the following errors:
10: return state.filter((thing) => { return thing !== action.payload })
^ property `payload`. Property not found in
6: | {| type: 'ACTION3' |} ^ object type
This is simply a matter of Flow aggressively invalidating type refinements. Flow does not know what filter is going to do with the callback you pass. Maybe it's going to save it and call it later. Flow also doesn't realize that nothing else reassigns action. As far as it's concerned, action might be reassigned to {type: 'ACTION3'} by the time the callback is called. Pulling the payload out into a const solves the issue:
const payload = action.payload;
return state.filter((thing) => { return thing !== payload })

Flowtype not detecting type of property inside object

I am hitting the following error
90: var b = action.data;
^^^^ property `data`. Property not found in
90: var b = action.data;
^^^^^^ object type
This is inside a function that receives action as an argument like this:
export default (state: SecurityGroupState = { groups: null, editingIPRange: null }, action: Action) => {
The type Action is imported using import type like so:
import type { Action } from "../../actions";
And it is declared as so:
export type Action = {
type: string,
data: Object,
} | {
type: string,
error: Object,
};
The code that is triggering the initial error is the following:
switch (action.type) {
case GET:
if (action.error) {
console.error(action.error);
break;
}
var a = action.data; // no error here
const groupsCopy2 = _.map(state.groups, () => {
var b = action.data;
});
}
So in the var a = ... line, Flow is OK with action.data, but inside the map lambda it doesn't seem to know that action: Action can have a data key.
Flow is pessimistic about refinements, it considers that every function call could modify action.data. As for a fix, you can use a const binding
const data = action.data
const groupsCopy2 = _.map(state.groups, () => {
var b = data;
});

Categories

Resources