Typer une application React/Redux avec Flow

+

+

=

@spyl94 ~ Aurélien David

Pourquoi typer JavaScript ?

 

Rend le code plus lisible

  • Facilite la compréhension et documente (JSDoc = 💩)
  • Permet un meilleur support au sein des IDE
  • Identifie facilement des erreurs de types
  • Améliore la performance d'exécution

Pour se simplifier la vie...

// Typed
function foo(x: string, y: number): number {
  return x.length * y;
}

// Not typed
function foo(x, y) {
  return x.length * y;
}

😀

😒

Devriez-vous utiliser un vérificateur de type? 🤔

 

Selon moi si un des points suivants est vrai:

 

  • Le besoin de refactoriser est récurrent
  • L'absence de bug est très critique
  • L'équipe de développement s'agrandit, ou change beaucoup
  • L'algorithmie est une part importante du code

Testing pyramid

Flow

  • http://flowtype.org/
  • Maintenu par Facebook
  • Flow is a static type checker, designed to quickly find errors in JavaScript applications
  • Pas un compiler, mais un outil d'analyse
  • Les annotations sont supprimées avant l'exécution

Lecture

➡️️

Analyse 

➡️️

Vérification

➡️️

✅ / ❌

  • Flow:
    • S'intègre facilement dans une chaine de build babel
    • Peut être activé fichier par fichier
  • TypeScript:
    • Chaine de build différente (tsc compiler)
    • S'intègre avec un loader webpack
  • Elm:
    • Syntaxe, sémantique et chaine de build différente

🚀 la barrière d'entrée est très faible pour une codebase existante

Pourquoi choisir Flow ? 😎

⚠️️  Je n'ai testé que Flow

Installation

// .babelrc
{
  "presets": ["react", "flow", "latest"],
}

En global: 

$ brew install flow

Ou au sein de votre projet:

$ yarn add --dev flow-bin

Ajout dans la chaine de build babel

$ yarn add babel-preset-flow

✂️️ La preset flow permet de supprimer les annotations lors du build

Utilisation

// index.js
// @flow

(function() {
    function foo(x: string, y: number): string {
        return x.length * y;
    }
    foo('Hello', 42);
});
$ flow # Démarre un serveur flow si besoin, puis affiche les erreurs:
index.js:6
  6:   return x.length * y;
              ^^^^^^^^^^^^ number. This type is incompatible with the expected return type of
  5: function foo(x: string, y: number): string {
                                         ^^^^^^ string
  1. On ajoute le tag @flow pour indiquer que ce fichier doit être vérifié
  2. On ajoute les annotations de type des paramètres
  3. On ajoute l'annotation du type de retour (avec une erreur)

4. On vérifie qu'on a bien une erreur en lançant flow:

Vérifier les librairies externes

$ yarn global add flow-typed

Les définitions de bibliothèques (libdef) décrivent les interfaces et les types utilisés par les librairies

 

$ yarn install # Install your dependencies
$ flow-typed install # Generates a `flow-typed` directory containing libdefs
# flow-typed/npm/is-url_v1.x.x.js

declare module 'is-url' {
  declare module.exports: (url: string) => boolean
}

Example de libdef pour le package is-url

flow-typed est un outil permettant d'installer facilement les libdefs

🤓 Evidemment, vous pouvez également écrire les vôtres!

Configuration

# .flowconfig

[ignore] # Files not visible
# ignore compiled files
.*/public/*
# ignore module source to prefer libdefs
.*/node_modules/*
.*/bower_components/*
# large dirs that are not imported
.*/vendor/*

[libs] # Directory containing libdefs
flow-typed

[options]
all=false # Only files annoted by @flow are checked
$ touch .flowconfig

Pro Tips:

$ flow ls # Liste les fichiers visible, pratique pour configurer la section [ignore]
emoji=true # Activer cette option pour ajouter des emojis aux réponses du serveur \o/
all=true # Sur une nouvelle codebase pour éviter d'avoir à annoter avec @flow

Intégration au sein des IDE

  • Code Diagnostic
  • Autocomplete
  • Jump To Definition
  • Type Hinting
  • Type Coverage

Excellent support sur Nuclide

Annotations usuelles

Primitifs disponibles:

let array: Array<number> = [1, 2, 3.14, 42];
let theAnswer: number = array[3]; // 42
let offTheEnd: number = array[100]; // No error

let tuple: [string, number, boolean] = ["foo", 0, true];

Tableaux :

Maybe: 

// '?' allow null and undefined
var o: ?string = null; 
type Rank = 0 | 1 | 2 | 3 | 4 | 5;

Union: 

Annotations d'objets

let object: {foo: string, bar: number} = {foo: "foo", bar: 0};

// Property writes must be compatible with the declared type.
object.bar = "bar"; // Error: "This type is incompatible with number"

let coolRating: {[id:string]: number} = {};
coolRating["sam"] = 10;
coolRating["paul"] = "cool"; // Error: "This type is incompatible with number"


// Optional properties
var optObj: { a: string; b?: number } = { a: "hello" };


// Burger syntax
type User = { name: string, age: number }; // allow extra properties
type StrictUser = {| name: string, age: number |}; // disallow extra properties

Annotations faibles

let object: Object;
let a: any;
let mix: mixed;

Annotations de fonctions

function greatestCommonDivisor(a: number, b: number): number {
  if (!b) {
    return a;
  }

  return greatestCommonDivisor(b, a % b);
}

// arrow functions
const double = (num: number): number => num * 2;

Fonctions ordinaires, asynchrones, promesses et générateurs sont supportés !

// async functions

async function getFriendNames(
  friendIDs: Promise<number[]>,
  getFriendName: (id: number) => Promise<string>,
): Promise<string[]> {
  var ids = await friendIDs;
  var names = await Promise.all(ids.map(getFriendName));
  return names;
}

Flow + React

I wouldn't expect there to be further changes to PropTypes. Flow has become much more mature recently, and from what I heard from the React team, it is the longer term solution to type checking.

 isRequired est équivalent à dire non null...

🙄 Comment gérer les cas où null est possible ?

Les problèmes de PropTypes...

PropTypes n'est utile qu'au runtime

🙄 Résultat non intégré à l'IDE !

PropTypes & Flow

// @flow
const Greeter = React.createClass({
  propTypes: {
    name: React.PropTypes.string.isRequired,
  },
  render() {
    return <p>Hello, {this.props.name}!</p>;
  },
});

<Greeter />; // Error: Missing `name`
<Greeter name={null} />; // Error:  `name` should be a string
<Greeter name="World" />; // Error: "Hello, World!"

✅ Support par défaut des PropTypes 

💪 Plus besoin d'exécuter notre code pour voir nos erreurs de passage de props ! 

Des PropTypes aux Props

type Props = {
  title: string,
  visited: boolean,
  onClick: () => void,
};

class Button extends React.Component {
  props: Props;

  static defaultProps: { visited: boolean };

  constructor(props: Props) {
    super(props);
    this.state = { display: 'static' };
  }

  render() {
    /* ... */
  }
}
type Props = {message: string};

function SayAgain({ message }: Props)
{
  return (
    <div>
      <p>{message}</p>
      <p>{message}</p>
    </div>
  );
}

React.Component

Stateless functional components

Les Higher-order components sont également supportés! 

Redux + Flow

type Action = IncrementAction | DecrementAction;
export const reducer = (state: State = initialState, action: Action): State => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
};

Typer un reducer

3. Décrire le reducer

  1. Décrire le state
// @flow
type State = number;
const initialState: State = 42;

2. Décrire chacune des actions

type IncrementAction = { type: 'INCREMENT' };
type DecrementAction = { type: 'DECREMENT' };

export const increment = (): IncrementAction => ({ type: 'INCREMENT' });
export const decrement = (): DecrementAction => ({ type: 'DECREMENT' });

Redux + Flow

// @flow ./redux/vote.js
import type { Dispatch, Action } from '../types'; // Cf prochain slide

// ...
export const deleteVote = (dispatch: Dispatch): void => {
   fetch('...').then(() => {
        dispatch({ type: 'DELETE_VOTE'});
   });
};

export const reducer = (state: State = initialState, action: Action): State => { /* */ }

3. On importe les types Action et Dispatch, pour décrire notre reducer et les fonctions du module

Typer plusieurs reducers (1/2)

1. On décrit à nouveau le state du reducer ainsi que ses actions

2. Cette fois, on exporte son state et la liste de ses actions

// @flow ./redux/vote.js
type VoteValue = 1 | 0 | -1;
type AddVoteAction = { type: 'VOTE', value: VoteValue };
type DeleteVoteAction = { type: 'DELETE_VOTE' };

export type State = { vote: ?VoteValue };
export type VoteAction = AddVoteAction | DeleteVoteAction;

Redux + Flow

import type { Store as ReduxStore, Dispatch as ReduxDispatch } from 'redux';

export type Store = ReduxStore<State, Action>; // Application store
export type Dispatch = ReduxDispatch<Action>;  // Dispatch only accept type Action

Typer plusieurs reducers (2/2)

1. On importe le type State de nos différents reducers afin de définir le State global de notre application

// @flow ./types.js
import type { State as CounterState, CounterAction } from './redux/counter';
import type { State as VoteState, VoteAction } from './redux/vote';

export type State = {
  counter: CounterState,
  vote: VoteState
};

2. On décrit la liste de toutes les actions possibles

export type Action =
    CounterAction |
    VoteAction |
;

ℹ️️ On l'utilise pour typer le paramètre action dans nos reducers

3. On décrit notre store et notre dispatcher

Redux + Flow 

Bénéfices

  • On ne peut désormais dispatch que les actions listées
  • Détection du code non utilisé (indiqué comme non couvert)
  • Les erreurs sont détectées instantanément
  • Un autocomplete qui comprend notre code
  • Documentation vivante de toutes les actions possibles

Pro Tips

  • Plus besoins des constantes, Flow vérifie action.type à notre place
  • Définir les types globaux au sein d'un même fichier réduit le nombre d'import de type
// @flow components/Content.js
import React from 'react';
import { connect, type Connector } from 'react-redux';
import type { State, Dispatch } from '../types';

type Props = {|  // Use exact object type for Props
    count: number,
    increment: () => void
|};

const Content = ({ count, increment }: Props) => (/* ... */);

const mapDispatchToProps = (dispatch: Dispatch) => ({
  increment: () => { dispatch({ type: 'INCREMENT'}) }, // Ou un action creator
});
const mapStateToProps = (state: State) => ({ count: state.counter });

// Connector<OwnProps, Props>
const connector: Connector<{}, Props> = connect(mapStateToProps, mapDispatchToProps);
export default connector(Content);

Typer les composants connectés

✅ Vérification des props compatible avec `connect`

React + Redux + Flow

✅ Une props inconnue =

✅ Dispatcher une action inconnue =

ℹ️️ Pas besoin de rendre explicite le type de retour de notre composant, Flow le déduit parce qu'il comprend JSX. (Type deviné: React$Element<any>)

Static vs Runtime

 

Flow est pratique pour analyser le code pendant qu'on l'écrit mais il ne peut prédire les types lors de l'exécution réelle du code, comme le résultat d'une requête à une API REST.

type Person = {
  name: string;
};

function greet (person: Person): string {
  return 'Hello ' + person.name;
}
import t from 'flow-runtime';
const Person = t.type('Person', t.object(t.property('name', t.string())));


function greet(person) {
  let _personType = Person;

  const _returnType = t.return(t.string());

  t.param('person', _personType).assert(person);

  return _returnType.assert('Hello ' + person.name);
}

t.annotate(greet, t.function(t.param('person', Person), t.return(t.string())));

⬇️️

ESLint plugin

$ yarn add --dev eslint-plugin-flowtype
{
  "parser": "babel-eslint",
  "plugins": [
    "flowtype"
  ],
  "rules": {
    'flowtype/boolean-style': ['error', 'boolean'],
    'flowtype/define-flow-type': 1,
    'flowtype/delimiter-dangle': ['error','never'],
    'flowtype/generic-spacing': ['error', 'never'],
    'flowtype/no-primitive-constructor-types': 'error',
    'flowtype/no-weak-types': ['error', {'any': false, 'Object': false }],
    'flowtype/object-type-delimiter': ['error', 'comma'],
    # ...
  }
}

Ajouter le support de Flow à notre linter

❤️️ Pratique pour éviter d'abuser des types faibles

⚠️️ Ne pas être trop strict lorsqu'on part d'une codebase existante

🙁 Pas de support de Flow dans le parser par défaut

Flow coverage

$ flow coverage app/js/store.js

Fichier par fichier:

$ flow-coverage-report -i "app/**/*.js"

Application:

C'est la mesure de qualité de votre analyse statique

Intégration continue

Avant de finir...

Objectif recommandé: viser > 90% de couverture

// package.json
{
   "scripts": {
       "typecheck": "flow check"
   }
}
$ yarn run typecheck # Add this !

Merci ! Questions ?

On travaille dur pour mettre à jour la démocratie... Vous nous donnez un coup de pouce ? 🙏

Aurélien David - @spyl94 - spyl.net

Démo de React + Redux + Flow: spyl94/react-brunch-demo