Import flowtype disjoint union? - javascript

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.

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

TypeScript: Idiomatic way to do a switch-case on Enum to set a variable

Example problem: I have an Enum variable Difficulty. In a function, I want to set the config DifficultyConfig depending on the value of Difficulty. Here's an inelegant way that I can think of:
export interface DifficultyConfig {
healthModifier: number,
deathIsPermanent: boolean,
}
export interface AppProps {
difficultyConfig: DifficultyConfig
}
export const NormalDifficultyConfig: DifficultyConfig = {
enemyHealthModifier: 1,
deathIsPermanent: false,
}
export const HigherDifficultyConfig: DifficultyConfig = {
enemyHealthModifier: 1.3,
deathIsPermanent: true,
}
export enum Difficulty {
NORMAL = 'Normal',
HARD = 'Hard',
ADVANCED = 'Advanced',
}
function createApp(difficulty: Difficulty) {
let difficultyConfig: DifficultyConfig;
switch(difficulty) {
case Difficulty.NORMAL:
difficultyConfig = NormalDifficultyConfig;
break;
// Both HARD and ADVANCED get HigherDifficultyConfig
case Difficulty.HARD:
difficultyConfig = HigherDifficultyConfig;
break;
case Difficulty.ADVANCED:
difficultyConfig = HigherDifficultyConfig;
break;
default:
difficultyConfig = NormalDifficultyConfig;
break;
}
return new App({
difficultyConfig
});
}
I'm not fond of the Switch case syntax for something so simple. My ideal would be something like this in Scala:
val difficultyConfig = difficulty match {
case Difficulty.NORMAL => NormalDifficultyConfig
case Difficulty.HARD | Difficulty.ADVANCED => HigherDifficultyConfig
case _ => NormalDifficultyConfig
}
Is there an equivalent for this in JavaScript?
If there's a difference in logic, a switch or an if/else if/else (slightly less verbose) is probably the way to go (more on this below though). If it's purely data as in your example, then you could use a difficulty-to-config mapping object:
const difficultyConfigs = {
[Difficulty.NORMAL]: NormalDifficultyConfig,
[Difficulty.HARD]: HigherDifficultyConfig,
[Difficulty.ADVANCED]: HigherDifficultyConfig,
} as const;
You might even declare that as Record<Difficult, DifficultyConfig> like this, as caTS points out in the comments:
const difficultyConfigs: Record<Difficult, DifficultyConfig> = {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Difficulty.NORMAL]: NormalDifficultyConfig,
[Difficulty.HARD]: HigherDifficultyConfig,
[Difficulty.ADVANCED]: HigherDifficultyConfig,
} as const;
More on that in a minute, but either way, then the function is just:
function createApp(difficulty: Difficulty) {
let difficultyConfig = difficultyConfigs[difficulty];
return new App({
difficultyConfig
});
}
Playground links: Without Record | With Record (I also updated a couple of seeming typos/editing errors in the question's code.)
This also has the advantage that if you add a Difficulty but forget to include one in difficultyConfigs, you get a handy compile-time error when you use it. I've simulated such an error here by adding a MEDIUM difficulty but "forgetting" to update the function.
But better yet, if we include the type Record<Difficulty, DifficultyConfig> type as caTS suggested and difficultyConfigs doesn't have an entry for every Difficulty value, you get an even earlier compile-time error, like this.
Even for logic, if you like you can use the same sort of concept to create a dispatch object:
const difficultyConfigs: Record<Difficulty, () => DifficultyConfig> = {
[Difficulty.NORMAL]: () => { /*...build and return NORMAL config...*/ },
[Difficulty.HARD]: () => { /*...build and return HARD config...*/ },,
[Difficulty.ADVANCED]: () => { /*...build and return ADVANCED config...*/ },,
} as const;
// ...
const difficultyConfig = difficultyConfigs[difficulty]();

Issue in Typing a class with Flowjs

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.

refining flow union type

Please see the code below. I have to do two casting to avoid any flow error. If I use the commented out lines instead, it complains.
playground
/* #flow */
import * as React from "react";
type ConfObj = { label: string };
type Conf = React.Node | ConfObj;
type MyComponentProp = {
confs: Array<Conf>,
}
export default function MyComponent({
confs = [],
}: MyComponentProp) {
const items = confs.map((item, idx) => {
if (React.isValidElement(item)) {
// return React.cloneElement(item, {
return React.cloneElement(((item: any): React.Element<*>), {
key: idx.toString(),
});
}
const item2 = ((item: any): ConfObj);
return <span>{item2.label}</span>;
// return <span>{item.label}</span>;
});
return <div>items</div>
}
Is there a better way to do this to avoid the casting. Is there a better way to write isValidElement, so flow can deduce the type once the if condition matches. For example, if it is a valid react element, why do I need to cast it? or if it not, why accessing label gives error?
An item is of type Conf (which is Node | ConfObj)
When you enter the if statement, flow doesn't know for sure that item is a valid Element<*> (this could be known by flow I think tho), so you have to explicitly typecast it.
The <span>{item.label}</span> has the same problem. You also have to explicitly typecast it to a ConfObj, because a Node doesn't have a label attribute.

Redux.js: catching malformed actions?

Is there a best practice/recommended way to assert that a redux action is well-formed? I am a fairly noobish JavaScript programmer (coming at it from 20 years of C++/Java/C#) and thrown off by the lack of strong typing.
Specifically the use case I am trying to address is:
1.using the React + Redux "ToDo" app (http://redux.js.org/docs/basics/ExampleTodoList.html)
2.using action creator:
export function toggleTodo(index) {
return { type: TOGGLE_TODO, index }
}
3.with reducer code snippet:
case TOGGLE_TODO:
if (state.id !== action.id) {
return state
}
Notice that index and id don't match up. However, they should have - it was a bug. This took me 30 minutes to diagnose, and I can only imagine for larger apps.
Have you considered creating a Class that represents an Action type, and then letting it handle the validation, like so...
class ToggleAction {
constructor(o) {
if (
typeof o === 'object' &&
typeof o.type === 'string' &&
typeof o.id === 'number'
) {
this.type = "TOGGLE_TODO";
this.id = o.id
} else {
throw new Error('Invalid ToggleAction');
}
}
toObject() {
return { type: this.type, id: this.id };
}
}
And then you could use it like this in the action creator...
export function toggleTodo(index) {
return new ToggleAction({ id: index }).toObject();
}
And like this in the reducer...
case TOGGLE_TODO:
const toggleAction = new ToggleAction(action)
if (state.id !== toggleAction.id) {
return state
}
If all of that works out well, you could create an ActionFactory that generated ActionType classes.
EDIT: I created an npm module called redux-action-validator with a README describing how to install and use it.

Categories

Resources