2 avril 2019
Typage de bout en bout avec TypeScript, GraphQL et React
5 minutes de lecture
JavaScript est un langage très puissant et flexible mais lors de la création d'applications lourdes avec des contraintes de données strictes il atteint parfois ses limites.
De nombreux outils permettent d'identifier des problèmes en amont comme ESLint, mais ce n'est qu'un premier rempart.
Dans cet article, nous allons voir comment utiliser TypeScript avec GraphQL et React afin de développer des applications robustes. La clé ? Un typage de A à Z : de la base de données jusqu'à vos vues applicatives !
Serveur GraphQL
L'API constitue votre première brique applicative, celle-ci est construite avec Apollo Server permettant d'implémenter GraphQL ainsi que TypeORM pour fournir l'accès à vos données.
L'ensemble de la partie serveur expliqué ci-dessous est disponible sur ce CodeSandbox.
Typage ORM
TypeORM est un ORM écrit en TypeScript utilisant les dernières fonctionnalités du langage, on peut ainsi y définir nos entités via les décorateurs et le typage. Pour notre exemple nous allons créer une classe User
.
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
email: string
@Column({ nullable: true })
name: string
@Column()
password: string
}
Schéma GraphQL
Pour exposer les données de l'utilisateur il faut ensuite définir un schéma ainsi qu'une query GraphQL. Notez que nous ne souhaitons pas exposer le mot de passe de l'utilisateur et que les contraintes de valeurs nulles sont identiques au mapping de la base de données.
Un mauvais mapping entraînera des erreurs lors de la récupération des résultats : nous possédons déjà une première couche de validation de nos données !
import { gql } from 'apollo-server-express'
const User = gql`
type User {
id: Int!
email: String!
name: String
}
extend type Query {
me: User!
}
`
export default User
Génération des types serveur
Nous avons désormais nos définitions de types de base de données via TypeORM et GraphQL avec le schéma, nous pourrions maintenant définir des types TypeScript pour nos Query et Mutation. Pour se faciliter la vie nous allons utiliser l'outil graphql-codegen qui va se charger de générer tout nos types à partir du schéma.
La configuration dans le fichier codegen.yml est la suivante, nous voulons du typing TypeScript et nous ajoutons le plugin typescript-resolvers
pour générer les types de nos resolvers.
schema: https://xp3wwv5lyo.sse.codesandbox.io/
generates:
server-types.ts:
- typescript
- typescript-resolvers
Il nous reste a écrire le resolver pour notre utilisateur, remarquez l'import du type Resolvers généré suite à la commande gql-gen --config codegen.yml
.
Note : Il est aussi possible de lancer les commandes de génération en mode watch pour accélérer le développement.
import { getRepository } from 'typeorm'
import { IResolvers } from './generated/server-types'
import User from './entity/User'
const resolvers: IResolvers = {
Query: {
me: (root, args, context) =>
getRepository(User).findOne({
select: ['id', 'email', 'name'],
where: { email: 'hello@premieroctet.com' },
}),
},
}
Ce type importé correspond en effet au resolvers de notre applicatif. Ceci conclut notre API GraphQL typée, maintenant voyons comment la consommer côté client.
Front React
Pour la partie front nous utiliserons React avec create-react-app qui supporte maintenant le TypeScript, ainsi que le client Apollo pour envoyer des requêtes à notre API.
L'ensemble de la partie cliente expliquée ci-dessous est disponible sur ce CodeSandbox.
Génération des types client
Commençons par la création des types nécessaires à la définition de nos composants et requêtes. La configuration dans le fichier codegen.yml est la suivante, nous ajoutons les plugins typescript-client
et typescript-react-apollo
pour générer les types de nos composants.
overwrite: true
schema: https://xp3wwv5lyo.sse.codesandbox.io/
documents:
- 'src/graphql/**/*.{ts,tsx}'
generates:
src/generated/graphql.tsx:
config:
noNamespaces: true
noComponents: true
withHooks: true
plugins:
- typescript-common
- typescript-client
- typescript-react-apollo
Typage des composants
Nous regroupons souvent l'ensemble de nos requêtes GraphQL dans un dossier src/graphql
, c'est celui-ci qui sera parsé par l'outil codegen pour générer nos types et composants.
Commençons par écrire la requête pour récupérer l'utilisateur courant :
import gql from 'graphql-tag'
const GET_USER = gql`
query GetUser {
me {
id
email
name
}
}
`
Ici pas besoin de l'importer car nous n'allons pas l'utiliser directement. Nous allons générer les composants de requête à la place, toujours avec la commande gql-gen --config codegen.yml
.
Nous allons en profiter pour utiliser les hooks de React 16.8 afin d'avoir un code plus concis.
Dans notre fichier généré src/generated/graphql.tsx
nous pouvons voir tout nos types qui découlent de notre API ainsi que des composants : celui qui nous intéresse est le hook useGetUser
correspondant à notre query. En l'utilisant et en important les autres types le concernant, nous pouvons avoir des props et de la validation en accord avec notre schéma GraphQL, et tout cela de manière automatique !
import React from 'react'
import { useGetUser, GetUserMe } from './generated/graphql'
// Props are typed according to GraphQL query
type UserProps = {
user: GetUserMe,
}
// Components are typed with props definition
const UserComponent = ({ user }: UserProps) => <p>Hello {user.name}</p>
export default () => {
// react-apollo hook, we got autocompletion for data and graphql variables here
const { data, loading } = useGetUser()
return (
<div className="App">
<header className="App-header">
{/** Accessing an undefined key here will break the build */}
{loading && <p>"Loading"</p>}
{data && data.me && <UserComponent user={data.me} />}
</header>
</div>
)
}
Conclusion
Avec un peu de configuration et l'outil GraphQL Codegen nous avons automatisé la création de type pour notre API et notre front, et fiabilisé l'ensemble de notre application. Une modification du schéma comme par exemple la suppression ou le renommage d'une propriété, entraînera directement une erreur du compilation du front.
Ci-joint quelques erreurs que j'ai pu rencontrer lors de la création de ces exemples et qui me conforte dans le développement d'application avec typage.
Vérification des colonnes sélectionnable lors d'une requête avec TypeORM
Resolver avec type de retour invalide
Valeur nulle possible lors de la récupération des données dans un composant React
TypeScript est un outil puissant mais on se sent parfois beaucoup moins productif et on peut perdre du temps lors de typage complexe comme les HOC React, c'est donc vital de se simplifier la tâche et de rester pragmatique lors de l'utilisation de ce dernier. Je recommande l'article "Don't let TypeScript slow you down" qui explique bien cette approche.