Changer le thème

Configuration TypeScript avancée pour Next.js : optimiser tsconfig et la sécurité des types

Easton editorial illustration: server-client bridge

Sur le rapport de test, une ligne rouge criait : « Production Error: Cannot read property ‘id’ of undefined ». Les utilisateurs disaient que la page profil affichait un écran blanc. En creusant le code — la route était /users/profile au lieu de /user/profile, un s en trop. TypeScript n’avait rien dit, l’IDE non plus, et c’était en prod.

Une erreur « basique » ? Oui. Mais ce genre d’erreur « basique » revient souvent dans les projets que je maintiens. Fautes de frappe sur les routes, noms de variables d’environnement incorrects, paramètres de fonction en any partout… TypeScript promet la « sécurité des types », et pourtant on a parfois l’impression de ne pas être si loin du JavaScript.

Ce n’est pas TypeScript qui est faible — c’est la config. Dans tsconfig.json, des dizaines d’options sans savoir lesquelles activer ; les tutos se contredisent : certains disent que le strict alourdit le dev, d’autres que sans strict TypeScript ne sert à rien. Après presque un an de Next.js + TypeScript, les any traînaient encore partout.

Cet article résume ce que j’ai appris en un an de galères : optimiser tsconfig, routes typées, variables d’environnement typées — pour transformer TypeScript d’« obstacle » en « garde-fou ». Pas de théorie abstraite : du concret utilisable tout de suite.

Optimiser tsconfig — poser de bonnes bases

Comprendre ce que signifie vraiment le mode strict

Beaucoup (moi y compris, avant) pensent que strict: true est un simple interrupteur. Ce n’est pas le cas.

La doc officielle TypeScript montre que strict regroupe 7 options :

{
  "compilerOptions": {
    "strict": true,
    // Équivalent à ces 7 options à true
    "strictNullChecks": true,        // Vérification stricte null/undefined
    "strictFunctionTypes": true,     // Types de fonctions stricts
    "strictBindCallApply": true,     // bind/call/apply stricts
    "strictPropertyInitialization": true, // Initialisation stricte des propriétés
    "noImplicitAny": true,          // Interdit any implicite
    "noImplicitThis": true,         // Interdit this implicite
    "alwaysStrict": true            // Parse toujours en mode strict
  }
}

Les trois premières sont les plus utiles au quotidien. strictNullChecks traite null et undefined comme des types distincts, plus comme des valeurs valides pour n’importe quel type.

Exemple — requête utilisateur en base :

// Sans strictNullChecks
const user = await db.user.findOne({ id: userId })
console.log(user.name) // Pas d'erreur TS, mais user peut être null

// Avec strictNullChecks
const user = await db.user.findOne({ id: userId })
console.log(user.name) // ❌ Erreur : l'objet peut être null

// Il faut écrire
if (user) {
  console.log(user.name) // ✅ OK
}

La première fois que j’ai activé cette option sur un vieux projet, l’IDE affichait plus de 200 erreurs. J’ai failli tout désactiver. En regardant de près, ce sont des bugs potentiels — des endroits sans garde null qui explosent en prod.

noImplicitAny interdit les any implicites sur paramètres et variables :

// Sans noImplicitAny
function handleData(data) {  // data devient any
  return data.value  // Aucune erreur
}

// Avec noImplicitAny
function handleData(data) {  // ❌ Paramètre implicitement any
  return data.value
}

// Annotation explicite
function handleData(data: { value: string }) {  // ✅
  return data.value
}

Au début, c’est plus contraignant. Après quelques semaines, l’autocomplétion sur data. liste toutes les propriétés — fini de fouiller la doc.

Configuration TypeScript spécifique à Next.js

Le tsconfig d’un projet Next.js a quelques particularités. Voici la version que j’utilise aujourd’hui :

{
  "compilerOptions": {
    // Base
    "target": "ES2020",
    "lib": ["dom", "dom.iterable", "esnext"],
    "jsx": "preserve",
    "module": "esnext",
    "moduleResolution": "bundler",

    // Requis pour Next.js
    "allowJs": true,
    "noEmit": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "resolveJsonModule": true,

    // Strict (cœur)
    "strict": true,
    "skipLibCheck": true,

    // Performance
    "incremental": true,

    // Plugin Next.js
    "plugins": [
      {
        "name": "next"
      }
    ],

    // Alias de chemins
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/lib/*": ["./src/lib/*"],
      "@/styles/*": ["./src/styles/*"]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

Quelques points souvent négligés :

1. incremental : compilation incrémentale

60%
Gain de vitesse de compilation

Cette option accélère nettement les gros projets : TypeScript met en cache la compilation précédente et ne recompile que ce qui change. Sur un projet de 300+ composants, le temps est passé d’environ 45 s à 18 s.

2. paths : alias de chemins

Avant :

import Button from '../../../components/ui/Button'
import { formatDate } from '../../../../lib/utils'

Impossible de compter les .. ; un déplacement de dossier casse tout.

Avec les alias :

import Button from '@/components/ui/Button'
import { formatDate } from '@/lib/utils'

Plus lisible, inférence de types correcte, navigation IDE fonctionnelle.

3. plugins : plugin Next.js

"plugins": [{ "name": "next" }] permet à TypeScript de comprendre les spécificités Next.js — types des fichiers layout.tsx, page.tsx dans app, distinction Server/Client Components.

Sans ce plugin, les Server Components peuvent déclencher de fausses erreurs de type.

Activer le mode strict progressivement

Sur un projet déjà volumineux, passer strict: true d’un coup fait mal. Mieux vaut avancer par étapes.

Stratégie 1 : strict pour le neuf, l’ancien en douceur

Gardez strict: true dans tsconfig.json. Pour les fichiers pas encore migrés, en tête de fichier :

// @ts-nocheck  // Ignore tout le fichier

Ou ligne par ligne :

// @ts-ignore  // Ignore la ligne suivante

Différence entre @ts-ignore et @ts-expect-error :

// @ts-ignore
const x = 1 as any  // Pas d'avertissement même si la ligne suivante est OK

// @ts-expect-error
const y = 1  // Si la ligne suivante n'a pas d'erreur, TS signale un commentaire inutile

Je préfère @ts-expect-error : une fois le bug corrigé, TypeScript rappelle de retirer le commentaire.

Stratégie 2 : module par module

Par exemple, nettoyer d’abord components/, le reste en mode plus souple :

// tsconfig.strict.json (mode strict)
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "strict": true
  },
  "include": ["src/components/**/*"]
}

Développement quotidien avec tsconfig.json ; refactor d’un module avec la version strict.

Le strict n’est pas là pour vous embêter. En refactorant un vieux composant avec strictNullChecks, j’ai trouvé 5 gardes null manquantes — 3 avaient déjà causé des erreurs en prod, avalées par un try-catch. Les vagues rouges de l’IDE ont soudain semblé utiles.

Routes typées — fini les fautes de frappe

Typed Routes intégré à Next.js

Le bug du début — un s en trop, page 404 — est évitable.

Next.js 13 propose typedRoutes : TypeScript génère des types pour toutes vos routes.

Activation

Dans next.config.ts :

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    typedRoutes: true,  // Routes typées
  },
}

export default nextConfig

Redémarrez le serveur de dev (npm run dev). Next.js scanne app/ et génère les types dans .next/types.

À quoi ça ressemble

Structure exemple :

app/
├── page.tsx           // Accueil
├── blog/
│   ├── page.tsx      // Liste blog
│   └── [slug]/
│       └── page.tsx  // Article
└── user/
    └── [id]/
        └── profile/
            └── page.tsx  // Profil

Avec typedRoutes, Link et useRouter autocomplètent les chemins :

import Link from 'next/link'

export default function Nav() {
  return (
    <nav>
      <Link href="/">Accueil</Link>
      <Link href="/blog">Blog</Link>
      <Link href="/blog/hello-world">Article</Link>
      <Link href="/user/123/profile">Profil</Link>

      {/* ❌ Erreur TS : route inexistante */}
      <Link href="/users/123/profile" />  // users au lieu de user
    </nav>
  )
}

En tapant href="/, l’IDE liste les routes valides. Une faute = erreur immédiate.

Limites

  1. App Router uniquement — pas avec pages/
  2. Paramètres dynamiques manuels — pour /blog/[slug], vous assemblez slug vous-même
  3. Query string non typéetab dans /user?tab=settings n’est pas vérifié

En résumé : le chemin ne se trompe plus ; les valeurs de paramètres restent votre responsabilité.

Bibliothèque tierce : nextjs-routes

Avec pages/ ou pour typer aussi les query params, essayez nextjs-routes.

Installation :

npm install nextjs-routes

Dans next.config.ts :

const nextRoutes = require('nextjs-routes/config')

const nextConfig = nextRoutes({
  // Votre config Next.js existante
})

export default nextConfig

Usage :

import { route } from 'nextjs-routes'

const profileRoute = route({
  pathname: '/user/[id]/profile',
  query: {
    id: '123',
    tab: 'settings',  // Query typée aussi
  }
})

router.push(profileRoute)  // Entièrement typé

const wrongRoute = route({
  pathname: '/users/[id]/profile',  // ❌ Erreur : chemin inexistant
})

Par rapport au built-in : support pages/, query typée, objet route au lieu de concaténer des chaînes. Inconvénient : dépendance supplémentaire ; régénération auto des types à chaque changement de structure.

Inférence des paramètres de route

Pour app/blog/[slug]/page.tsx, Next.js fournit le type de params :

// app/blog/[slug]/page.tsx
export default function BlogPost({
  params,
}: {
  params: { slug: string }
}) {
  return <h1>Article : {params.slug}</h1>
}

slug reste un string générique. Pour restreindre le format, validation runtime avec zod :

import { z } from 'zod'

const slugSchema = z.string().regex(/^[a-z0-9-]+$/)

export default function BlogPost({
  params,
}: {
  params: { slug: string }
}) {
  const validatedSlug = slugSchema.parse(params.slug)

  return <h1>Article : {validatedSlug}</h1>
}

Slug invalide → erreur zod. Très utile sur les routes API où l’entrée utilisateur est imprévisible.

Variables d’environnement typées — éliminer les any

D’où vient le problème

Par défaut, TypeScript gère mal process.env.

const apiKey = process.env.API_KEY

Type : string | undefined. Déjà mieux que rien.

Mais souvent :

const apiUrl = process.env.NEXT_PUBLIC_API_URL
console.log(apiUrl.toUpperCase())  // Crash runtime : undefined

Pas d’erreur à la compilation. Et une faute de nom :

const key = process.env.API_SECRE  // T manquant
// TypeScript : string | undefined, tout va bien…

On utilise TypeScript mais on vérifie les noms à l’œil — comme en JavaScript pur.

T3 Env (recommandé)

La solution la plus adoptée : T3 Env — typage et validation à l’exécution.

Installation :

npm install @t3-oss/env-nextjs zod

Configurationenv.mjs (ou env.ts) à la racine :

import { createEnv } from "@t3-oss/env-nextjs"
import { z } from "zod"

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    API_SECRET: z.string().min(32),
    SMTP_HOST: z.string().min(1),
  },

  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
    NEXT_PUBLIC_ANALYTICS_ID: z.string().optional(),
  },

  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    API_SECRET: process.env.API_SECRET,
    SMTP_HOST: process.env.SMTP_HOST,
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
    NEXT_PUBLIC_ANALYTICS_ID: process.env.NEXT_PUBLIC_ANALYTICS_ID,
  },
})

Usage :

import { env } from './env.mjs'

const dbUrl = env.DATABASE_URL  // string
const appUrl = env.NEXT_PUBLIC_APP_URL  // string

// ❌ Faute de frappe
const wrong = env.DATABASE_UR

// ❌ Variable serveur côté client
'use client'
const secret = env.API_SECRET

Avantages :

  1. Validation au démarrage — variable manquante ou invalide = échec immédiat
  2. Types précis — plus de string | undefined partout
  3. Anti-fuite — accès client à une variable serveur = erreur de compilation

Avant T3 Env, l’environnement de test plantait souvent pour une variable oubliée ; il fallait lire les logs. Maintenant, c’est visible au boot.

Extension manuelle de ProcessEnv

Petit projet ou sans dépendance supplémentaire — env.d.ts :

// env.d.ts
namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string
    API_SECRET: string
    SMTP_HOST: string

    NEXT_PUBLIC_APP_URL: string
    NEXT_PUBLIC_ANALYTICS_ID?: string
  }
}
const dbUrl = process.env.DATABASE_URL  // string

// ❌ Erreur TS
const wrong = process.env.DATABASE_UR

Limites : pas de validation runtime ; pas de blocage client → serveur ; maintenance manuelle des types.

Pour un projet TypeScript sérieux, T3 Env reste le choix le plus sûr.

Mode strict en pratique

Types des bibliothèques tierces

Parfois le problème vient d’une lib sans types ou avec des définitions incorrectes.

Lib sans types

import oldLib from 'some-old-lib'  // any

Cherchez @types/some-old-lib :

npm install -D @types/some-old-lib

Sinon, types/some-old-lib.d.ts :

declare module 'some-old-lib' {
  export function doSomething(param: string): number
  export default someOldLib
}

Types incorrects

import { someFunction } from 'buggy-lib'

const result = someFunction() as number  // Contournement temporaire

Mieux : issue ou PR sur le dépôt de la lib.

skipLibCheck ?

Recommandation : oui.

Les erreurs dans node_modules ne sont pas les vôtres et ralentissent la build. Concentrez-vous sur votre code.

Fuites any courantes

Handlers d’événements

// ❌
const handleSubmit = (e: any) => {
  e.preventDefault()
}

// ✅
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault()
}

Types utiles : React.MouseEvent<HTMLButtonElement>, React.ChangeEvent<HTMLInputElement>, React.KeyboardEvent<HTMLDivElement>.

Réponses API

// ❌
const res = await fetch('/api/user')
const data = await res.json()  // any

// ✅ Interface
interface User {
  id: string
  name: string
  email: string
}

const data: User = await res.json()

// ✅ zod (recommandé)
import { z } from 'zod'

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
})

const data = UserSchema.parse(await res.json())

Import dynamique

// ❌
const module = await import('./utils')  // any

// ✅
const module = await import('./utils') as typeof import('./utils')

const { formatDate } = await import('./utils')

Types utilitaires intégrés

Pick

interface User {
  id: string
  name: string
  email: string
  password: string
  createdAt: Date
}

type PublicUser = Pick<User, 'id' | 'name' | 'email'>

Omit

type CreateUserInput = Omit<User, 'id' | 'createdAt'>

Partial

type UpdateUserInput = Partial<User>

Required

type RequiredUser = Required<Partial<User>>

Type utilitaire personnalisé

type PartialString<T> = {
  [K in keyof T]: T[K] extends string ? T[K] | undefined : T[K]
}

Au début intimidant ; une fois l’habitude prise, énorme gain sur les objets complexes.

Conclusion

Le bug de trois heures du matin aurait pu être évité : typedRoutes pour la route, T3 Env pour les variables, strict pour les any implicites.

La sécurité des types déplace les bugs du runtime vers l’édition. Mieux vaut un IDE rouge qu’un écran blanc en prod.

Récapitulatif :

  1. tsconfig : strict, incremental, paths, plugin Next.js
  2. Routes : typedRoutes (13+) ou nextjs-routes
  3. Env : T3 Env (typage + runtime)
  4. Pratique : strict progressif, libs tierces, éliminer les any courants

La config et les annotations demandent un effort au début ; l’autocomplétion précise et la détection précoce des bugs rendent difficile le retour au JavaScript « à nu ».

Ouvrez tsconfig.json et passez strict à true. Plus de soulignements rouges, plus de bugs potentiels repérés — c’est une bonne chose.

FAQ

Le mode strict ralentit-il la compilation du projet ?
Non. Le mode strict renforce seulement la vérification des types, sans impact notable sur la vitesse de compilation. Avec incremental, les gros projets gagnent souvent 30 à 50 % de temps de build.
Comment activer le mode strict en toute sécurité sur un ancien projet ?
Stratégie progressive : activez strict dans tsconfig.json, marquez les fichiers non migrés avec @ts-expect-error, imposez le strict sur le code neuf et refactorisez l'ancien par modules.
Quelle différence entre T3 Env et une extension manuelle de ProcessEnv ?
T3 Env valide à l'exécution au démarrage (variables manquantes ou mal formées) et empêche l'accès client aux variables serveur. ProcessEnv manuel ne couvre que la compilation, sans garde à l'exécution.
typedRoutes de Next.js fonctionne-t-il avec le dossier pages ?
Non. typedRoutes est une fonctionnalité expérimentale de Next.js 13+ pour l'App Router (dossier app). Avec pages, utilisez la bibliothèque tierce nextjs-routes.
skipLibCheck pose-t-il un risque de sécurité ?
Non. skipLibCheck ignore seulement la vérification des types dans node_modules ; votre code reste contrôlé. Les erreurs des libs tierces ne sont pas corrigeables par vous — mieux vaut gagner en vitesse et se concentrer sur votre code.

10 min de lecture · Publié le: 6 janv. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog