TypeScript

C'est quoi TypeScript ?

TypeScript is
a typed superset of JavaScript
that compiles to plain JavaScript.

En JavaScript


function greeter(person) {
  return "Hello, " + person;
}

greeter('Michel');

// Hello, Michel

function greeter(person) {
  return "Hello, " + person;
}

greeter({ firstname: 'Michel', lastname: 'Michel' });

// Hello, [object Object]

En TypeScript

On ajoute le typage (TypeScript)


function greeter(person: string) {
  return "Hello, " + person;
}

greeter('Michel');

// Hello, Michel

function greeter(person: string) {
  return "Hello, " + person;
}

greeter({ firstname: 'Michel', lastname: 'Michel' });

// Argument of type '{ firstname: string; lastname: string; }'
// is not assignable to parameter of type 'string'.

Attention !

Le code compilé reste du JavaScript "classique".


function greeter(person) {
  return "Hello, " + person;
}

greeter('Michel');
On peut aussi typer les retours de fonction :

function nbLetter(str: string): number {
  return str.length;
}
	
et des fonction fléchées :

const nbLetter = (str: string): number => str.length;
TypeScript, c'est PHPStan pour JavaScript

Les types "basiques"

Tableaux
				
				let list: number[] = [1, 2, 3]; 
			
ou alors la forme plus explicite :

				let list: Array<number> = [1, 2, 3];
			

Un peu de POO

Les interfaces

  • Pas d'interfaces en JavaScript
  • TypeScript vérifie la "forme" des objets
  • En TypeScript, les interface permettent juste de nommer une "forme"

				function printLabel(labeledObj: { label: string }) {
				  console.log(labeledObj.label);
				}
				
				let myObj = {size: 10, label: "Size 10 Object"};
				printLabel(myObj);
			

👍

"myObj" a bien une clé "label" de type "string"

On peut extraire la définition de "l'interface"

interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
			
Propriétés optionnelles

interface SquareConfig {
  width?: number;
}

function createSquare(config: SquareConfig): {area: number} {
  let newSquare = { area: 100 };
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({});
				

Les classes

Une classe peut imlémenter une interface


			interface ClockInterface {
			  currentTime: Date;
			  setTime(d: Date): void;
			}
			
			class Clock implements ClockInterface {
			  currentTime: Date = new Date();

			  constructor(h: number, m: number) { }

			  setTime(d: Date) {
			    this.currentTime = d;
			  }
			}
			

private, protected, public


class Human {
  // public par défaut
  firstname: string;
  // idem
  public lastname: string;

  // accessible aux enfants uniquement, mais en JS ça reste public
  protected religion: string;

  // accessible à personne, mais en JS ça reste public, à éviter
  private sex: string;
  // fonctionnalité ECMAScript, vraiment privé, à privilégier
  #salary: number;
}
			

Les fonctions

En JavaScript, les paramètres de fonctions sont tous facultatifs:

			  function buildName(firstName, lastName) {
			    console.log(firstName, lastName);
			  }

			  buildName(); // log : undefined, undefined
			  
En TypeScript, tous les paramètres sont requis, sauf si ils sont notés comme facultatifs:

			  function buildName(firstName: string, lastName?: string) {
			    console.log(firstName, lastName);
			  }
  
			  buildName(); // Erreur
			  buildName('Michel'); // OK, log: Michel, undefined
			  buildName('Michel', 'Michel'); // OK
			  buildName('Michel', 'Michel', 'Michel'); // Erreur
			  
Une interface peut définir une fonction:

			interface SearchFunc {
			  (source: string, subString: string): boolean;
			}
			
			let mySearch: SearchFunc;
			mySearch = function(source: string, subString: string): boolean {
			  let result = source.search(subString);
			  return result > -1;
			}

			function callMe(fn: SearchFunc) {
			  // ...
			}
			
On peut aussi les définir "inline

			let mySearch = function(source: string, subString: string): boolean {
			  let result = source.search(subString);
			  return result > -1;
			}

			function callMe(fn: (source: string, subString: string) => boolean) {
			  // ...
			}
			

Retours dynamiques de fonctions


function doWeirdStuff(x: number): number;
function doWeirdStuff(x: string): boolean;
function doWeirdStuff(x): any {
  if (typeof x === 'number') {
    return x * 2;
  }

  return x.length > 3;
}

let n = doWeirdStuff(8); // n est de type "number"
let b = doWeirdStuff('this is weird'); // n est de type "boolean"

Les génériques

Imaginez cette belle fonction

              function identity(arg) {
                return arg;
              }
            
Sans les génériques, il faudrait soit typer avec les valeurs,

              function identity(arg: number): number {
                return arg;
              }
            
soit noter les types en "any"

              function identity(arg: any): any {
                return arg;
              }
            

            function identity<T>(arg: T): T {
              return arg;
            }

            let output = identity<string>("myString");
            //       ^ = let output: string
            

            function identity<T>(arg: T): T {
              return arg;
            }

            let output = identity("myString");
            //       ^ = let output: string
            
Les classes peuvent aussi être "génériques" :

            class GenericNumber<T> {
              zeroValue: T;
              add: (x: T, y: T) => T;
            }
            
            let myGenericNumber = new GenericNumber<number>();
            myGenericNumber.zeroValue = 0;
            myGenericNumber.add = function(x, y) {
              return x + y;
            };
            
Un type générique peut étendre un autre type

            interface Lengthwise {
              length: number;
            }
            
            function loggingIdentity<T extends Lengthwise>(arg: T): T {
              console.log(arg.length);
              return arg;
            }

            loggingIdentity(3);
            // Argument of type 'number' is not assignable 
            // to parameter of type 'Lengthwise'.

            loggingIdentity({ length: 10, value: 3 }); // OK
            

Interface vs Type


                interface Animal {
                  name: string
                }
                
                interface Bear extends Animal {
                  honey: boolean
                }
                

                type Animal = {
                  name: string
                }
    
                type Bear = Animal & { 
                  honey: Boolean 
                }
                
Interfaces
proche du principe "ouvert/fermé" de la POO
Types
permet de faire plus de choses (union, tuples)

Types utilitaires

Typescript propose plusieurs types "utilitaires":
  • Partial<Type>, Required<Type>
  • Readonly<Type>
  • Record<Keys,Type>
  • Pick<Type, Keys>, Omit<Type, Keys>
  • etc.
voir les types utilitaires

React

Migrer un composant React en TypeScript :

            import PropTypes from 'prop-types';
            import React from 'react';

            function Controls({ zoomIn, zoomOut }) {
              return (
                <div>
                  <button onClick={zoomIn}>+</button>
                  <button onClick={zoomOut}>−</button>
                </div>
              );
            }
            Controls.propTypes = {
              zoomIn: PropTypes.func.isRequired,
              zoomOut: PropTypes.func.isRequired,
            };
           
Migrer un composant React en TypeScript :

            -import PropTypes from 'prop-types';
             import React from 'react';

            -function Controls({ zoomIn, zoomOut }) {
            +function Controls({ zoomIn, zoomOut }: ControlsProps): React.ReactElement {
               return (
                 // ...
               );
             }
            -Controls.propTypes = {
            -  zoomIn: PropTypes.func.isRequired,
            -  zoomOut: PropTypes.func.isRequired,
            -};
            +type ControlsProps = {
            +  zoomIn: () => void;
            +  zoomOut: () => void;
            +};
           

Pour les composants "classes"


import React, { Component, ReactElement } from 'react';

type State = {
  counter: number;
}
type Props = {
  reset: () => void;
  seatId: number | null;
};

class CountThisSeat extends Component<Props, State> {
  static defaultProps = {
    seatId: null,
  };

  constructor(props: Props) {
    super(props);
    this.state = {
      counter: 0,
    }
  }

  render(): ReactElement {
    // render component, it should be the same as JavaScript version
  }
}
              

Redux

Les reducers


            const SEND_MESSAGE = 'SEND_MESSAGE';
            const LOGIN = 'LOGIN';

            type SendMessageAction = {
              type: typeof SEND_MESSAGE;
              message: string;
            }

            type LoginAction = {
              type: typeof LOGIN;
              username: string;
            }

            type AppActionTypes = SendMessageAction | LoginAction;

            type AppState = {
              messages: Array<string>
              loggedId: boolean;
              username?: string;
            }

            const initialState: AppState = {
              messages: [],
              loggedId: false,
            }

            function appReducer(state = initialState, action: AppActionTypes) {
              switch (action.type) {
                // ...
              }
            }
              
              

Les actions


            // TypeScript infers that this function is returning SendMessageAction
            export function sendMessage(newMessage: string): AppActionTypes {
              return {
                type: SEND_MESSAGE,
                payload: newMessage
              }
            }
            
            export function login(username: string): AppActionTypes {
              return {
                type: LOGIN,
                username,
              }
            }
            

connect, useSelector, useDispatch, redux-thunk

Pas mal de cas différents, le mieux étant de lire la doc redux et react-redux sur le sujet.
Pro-tip : couper en trois les props venant du "state", du "dispatch" et les autres:

            type ReduxStateProps = {
              messages: Array<string>;
            }

            type DispatchProps = {
              sendMessage: (message: string): AppActionTypes;
            }
            
            type OwnProps = {
              theme: string;
            }

            type Props = ReduxStateProps & DispatchProps & OwnProps;

            function SomeComponent(props: Props): ReactElement {
              return (
                // ...
              );
            }

            

Guide de migration pratique

TOUS les fichiers contenant du JSX doivent avoir l'extension `.tsx` comtrairement au `.js` / `.jsx`
  1. Migrer les fichiers "utils"
  2. Migrer les fichiers react "utils" (contextes, hooks)
  3. Migrer avant tout les composants : simple "conversion" de prop-types
    1. composants les plus bas niveau (sans dépendances)
    2. Fichiers plus complexes. Un fichier ne devrait être converti à TS que si ses dépendances le sont.
    3. Séparer les props "redux" des autres
  4. Les reducers
  5. Les containers
  6. Les actions

Divers en vrac

Types généraux

N'utisez pas Object, Number, String, Boolean et Symbol.
Utilisez object, number, string, boolean et symbol (notez la majuscule).

undefined > null ?


            // avec null
            type Props = { id: null | number };
            function constructor ({ id = null }: Props) {}
            
            // avec undefined
            type Props = { id?: number };
            function constructor ({ id }: Props) {}
            
  • type plus lisible
  • une assignation de moins
  • les "key" react ne doivent pas être null, mais peuvent être undefined
  • genéralement la même chose, sauf en cas de comparaison stricte avec `null`

Default props avec valeur null


// imaginez ce composant
function Foo({ eventDateId }) {}

Foo.defaultProps = {
  eventDateId: null,
};
Foo.propTypes = {
  eventDateId: PropTypes.number,
};

// Ceci est plutôt faux. La vrai definition est:
Foo.defaultProps = {
  eventDateId: null,
};
Foo.propTypes = {
  eventDateId: PropTypes.oneOfType(
    PropTypes.number,
    PropTypes.null,
    ),
};

// et du coup en TypeScript:
Foo.defaultProps = {
  eventDateId: null,
};
Foo.propTypes = {
  eventDateId: number | null,
};

// là aussi, mieux avec `undefined`
Foo.defaultProps = {
  eventDateId: undefined,
};
Foo.propTypes = {
  eventDateId?: number,
};

Les prop-types

  • TypeScript = au moment de la compilation
  • prop-types = au moment de l'execution

Convertir les types TS en prop-types ? babel-plugin-typescript-to-proptypes

OF SCRIPT

Des questions ?