Routes dynamiques avec Next.js & Typescript

Publié il y a 11 mois~3 min

Le système de routing de Next.js, basé sur la structure du dossier pages, est à la fois simple et puissant. Il nous permet par ailleurs de créer des routes dynamiques, basées sur des query params (ex: articles de blog).

Dans ce contexte, et en utilisant Typescript avec les types fournis par Next.js, nous nous retrouvons pourtant avec une erreur de typage lorsque l'on utilise les méthodes de data fetching (getStaticProps ou getServerSideProps) mises à disposition par le framework 😖.

Comment résoudre ce problème ? La documentation n'est malheureusement pas très explicite sur ce cas d'usage, et pourtant la solution est relativement simple !

Sur la base d'un projet Next.js, nous allons voir comment adapter le typage des méthodes de data fetching de Next.js, et ainsi éviter toute erreur de ce type :

1Property 'slug' does not exist on type 'ParsedUrlQuery | undefined'

Pré-requis

  1. Node.js (>= 10.13)
  2. Un projet Next.js (existant ou généré via npx create-next-app) avec Typescript

Ce site web a servi de base à cet article, n'hésitez donc pas à vous référer au code source pour plus de détails !

Les types Next.js

Next.js met à disposition des fonctions spécifiques pour la récupération de données, que l'on soit en génération statique (SSG) ou côté serveur (SSR).

Voyons ici le cas de la génération statique, avec les fonctions getStaticProps et getStaticPaths (la logique est similaire pour la méthode getServerSideProps).

Comme l'indique la documentation, Next.js met à disposition des types dédiés à chacune de ces fonctions. Pourtant dans le cas des routes dynamiques, l'utilisation du type tel qu'indiqué dans la documentation ne suffit pas :

1import type { ReactElement } from 'react'
2import type { GetStaticProps, GetStaticPaths } from 'next'
3import { Post, getPost, getPosts } from '@api/posts'
4
5interface BlogPostProps {
6 post: Post<'title' | 'content' | 'excerpt' | 'readingTime'>
7}
8
9const BlogPost = ({ post }: BlogPostProps): ReactElement => {
10 // ...
11}
12
13export default BlogPost
14
15export const getStaticProps: GetStaticProps = async ({
16 params,
17}) => {
18 /*
19 Here is the error:
20 Properties 'slug' and 'lang' don't exist on type 'ParsedUrlQuery | undefined'
21 */
22 const { lang, slug } = params
23 const post = getPost(
24 slug,
25 ['title', 'content', 'excerpt', 'readingTime'],
26 lang
27 )
28
29 return {
30 props: { post },
31 }
32}

Fort heureusement, si l'on examine les définitions des types GetStaticProps et GetStaticPaths, via notre IDE ou le code source, on se rend compte qu'il s'agit en fait de types génériques, que l'on peut donc étendre pour leur donner connaissance des spécificités de notre projet ! 🕺

Génériques !

Il ne reste donc plus qu'à définir une interface spécifique aux paramètres de requêtes de notre projet, qui étendra l'interface de base ParsedUrlQuery, fournie par Next.js...

1import type { ParsedUrlQuery } from 'querystring'
2import type { Locale } from './i18n'
3
4export interface QParams extends ParsedUrlQuery {
5 slug?: string
6 lang?: Locale
7}

... que l'on ajoutera comme argument au type générique de notre fonction :

1import type { GetStaticProps, GetStaticPaths } from 'next'
2import type { QParams } from '@typings/global'
3
4import { Post, getPost, getPosts } from '@api/posts'
5
6// ...
7
8export const getStaticProps: GetStaticProps<BlogPostProps, QParams> = async ({ params }) => {
9 // ...
10}
11
12export const getStaticPaths: GetStaticPaths<QParams> = async () => {
13 // ...
14}

Dans certains cas de paramètres de requêtes (comme pour ce site, utilisant un paramètre lang pour la gestion du multilingue), il est probable que le typage de votre fichier _document.tsx retourne lui aussi une erreur... Là encore, il suffira d'étendre l'interface initialement fournie par Next.js (DocumentInitialProps) :

1import type { DocumentInitialProps } from 'next/document'
2import type { Locale } from './i18n'
3
4export type MyDocumentProps = DocumentInitialProps & {
5 lang: Locale
6}

🎉

Et voilà ! Si la solution se trouvait (comme souvent) dans le code source, on peut toutefois regretter que la documentation officielle ne fasse pas mention de cet usage, pourtant assez fréquent...

Quoi qu'il en soit, nous pouvons désormais adapter facilement nos typages aux besoins spécifiques de notre projet... Sans oublier la règle absolue que l'on oublie peut-être trop souvent : read the f*** manual! 😄