Nehos Groupe

Ce qu'il faut retenir

TypeScript 5 apporte des fonctionnalités majeures qui transforment la DX sur les projets Next.js : les decorators ES2023 natifs (enfin standardisés), les const type parameters pour des inférences ultra-précises, et NoInfer pour bloquer les inférences non souhaitées dans les génériques.

Le pattern gagnant en 2026 associe TypeScript statique + Zod runtime : les schémas Zod servent de source unique de vérité pour les types ET la validation à l'exécution, ce qui élimine la duplication entre types et validateurs — particulièrement critique pour les Server Actions et les Route Handlers.

La performance du type-checker est un enjeu réel sur les monorepos Next.js de taille importante : tsc --incremental, skipLibCheck ciblé et isolatedModules combinés peuvent réduire les temps de vérification de 60 à 75 %, sans sacrifier la sécurité de typage sur le code métier.

TypeScript 5 avec Next.js 2026 : nouveautés et meilleures pratiques

Decorators ES2023 natifs, const type parameters, NoInfer, types pour Server Actions et Route Handlers, pattern Zod + TypeScript, tsconfig optimale et performance tsc — le guide de référence pour les équipes B2B qui construisent sur Next.js App Router.

Adapté à toute taille de structure

Artisan
Startup
PME / TPE
ETI
Grand Groupe
C
Chokri Siala
··next-headless

#TypeScript 5 en 2026 : les apports fondamentaux pour les projets Next.js

TypeScript 5 a introduit, entre la version 5.0 (mars 2023) et les releases mineures successives jusqu'en 2025, un ensemble de fonctionnalités qui redéfinissent les standards de qualité pour les projets Next.js en production. En 2026, ces features sont toutes stables et activées par défaut dans les projets créés avec create-next-app.

Le contexte général : TypeScript est désormais le langage par défaut dans l'écosystème Next.js. Plus de 85 % des nouveaux projets Next.js démarrent avec TypeScript selon le State of JS 2025. Vercel a d'ailleurs migré l'ensemble de ses propres projets vers TypeScript 5 strict mode. La question n'est plus de savoir si utiliser TypeScript, mais comment l'utiliser de façon optimale.

Les quatre piliers de TypeScript 5 pour les équipes Next.js B2B :

1. Sécurité de type plus précise : les const type parameters et les variadic tuple types améliorent l'inférence dans les fonctions génériques complexes, réduisant le besoin d'assertions manuelles (as Type) qui masquaient des bugs potentiels.

2. Decorators standardisés : après des années d'expérimental, les decorators ES2023 sont enfin alignés avec le standard TC39 Stage 3. Ils ouvrent des patterns d'architecture propres dans les Server Components et les services injectable.

3. Types utilitaires enrichis : NoInfer<T>, Awaited<T> affiné, et d'autres utilities réduisent la quantité de type-gymnastics nécessaire pour exprimer des contraintes métier complexes.

4. Performance du compilateur : les améliorations du mode --incremental et de isolatedModules rendent les projets monorepo Turborepo/Next.js significativement plus rapides à analyser.

Cet article couvre chacun de ces axes avec des exemples concrets tirés de projets B2B Next.js App Router.


#Decorators ES2023 natifs : usage dans Next.js App Router

Avant TypeScript 5.0, les decorators TypeScript utilisaient l'ancienne spec (Stage 1) avec experimentalDecorators: true dans tsconfig. TypeScript 5 implémente la spec Stage 3 finalisée, désormais partie du standard ECMAScript 2023.

#La différence fondamentale avec les anciens decorators

Les nouveaux decorators ne modifient plus la valeur de this de la même façon. Ils retournent une nouvelle valeur ou undefined, et ne peuvent plus remplacer silencieusement la cible décorée. C'est plus strict, donc plus sûr.

// Decorator de logging pour les Server Actions
function logAction(label: string) {
  return function <T extends (...args: unknown[]) => Promise<unknown>>(
    target: T,
    context: ClassMethodDecoratorContext
  ): T {
    return async function (this: unknown, ...args: Parameters<T>): Promise<ReturnType<T>> {
      const start = performance.now()
      console.log(`[Server Action] ${label} — start`)
      const result = await target.apply(this, args)
      const duration = performance.now() - start
      console.log(`[Server Action] ${label} — ${duration.toFixed(1)}ms`)
      return result as ReturnType<T>
    } as T
  }
}

// Usage dans une classe de service
class ContactService {
  @logAction('submitContactForm')
  async submitContactForm(data: ContactFormData): Promise<{ success: boolean }> {
    // Logique métier
    return { success: true }
  }
}

#Pattern : decorator de validation avec Zod

import { z } from 'zod'

function validate<S extends z.ZodSchema>(schema: S) {
  return function <T extends (input: z.infer<S>) => Promise<unknown>>(
    target: T,
    context: ClassMethodDecoratorContext
  ): T {
    return async function (this: unknown, input: unknown): Promise<ReturnType<T>> {
      const parsed = schema.safeParse(input)
      if (!parsed.success) {
        throw new Error(`Validation failed: ${parsed.error.message}`)
      }
      return target.call(this, parsed.data) as ReturnType<T>
    } as T
  }
}

const ProjectSchema = z.object({
  name: z.string().min(3).max(100),
  budget: z.number().positive(),
  deadline: z.coerce.date()
})

class ProjectService {
  @validate(ProjectSchema)
  async createProject(data: z.infer<typeof ProjectSchema>) {
    // data est garantie valide ici
    return await db.project.create({ data })
  }
}

#Note importante : cohabitation avec experimentalDecorators

Si votre tsconfig utilise encore experimentalDecorators: true, les deux syntaxes sont incompatibles dans le même fichier. La migration se fait fichier par fichier. Pour de nouveaux projets Next.js, ne pas activer experimentalDecorators — la nouvelle syntaxe est la référence.


#Const type parameters et variadic tuple types : cas d'usage avancés

#Const type parameters

Introduit dans TypeScript 5.0, le modificateur const sur un paramètre de type force l'inférence en mode littéral — comme si vous aviez passé as const sur chaque argument.

// Sans const — perd la précision des types littéraux
function createRoute<T extends string>(path: T, methods: string[]) {
  return { path, methods }
}
const route = createRoute('/api/users', ['GET', 'POST'])
// type: { path: string; methods: string[] }  <-- trop large

// Avec const — préserve les types littéraux
function createRoute<const T extends string, const M extends readonly string[]>(
  path: T,
  methods: M
) {
  return { path, methods } as const
}
const route = createRoute('/api/users', ['GET', 'POST'])
// type: { path: '/api/users'; methods: readonly ['GET', 'POST'] }  ✓

#Application Next.js : router type-safe

// Définition de routes type-safe avec const type parameters
const routes = [
  createRoute('/blog', ['GET']),
  createRoute('/api/contact', ['GET', 'POST']),
  createRoute('/api/newsletter', ['POST']),
] as const

type AppRoute = typeof routes[number]['path']
// type AppRoute = '/blog' | '/api/contact' | '/api/newsletter'

// Link type-safe dans les composants
function AppLink<T extends AppRoute>({ href, children }: { href: T; children: React.ReactNode }) {
  return <a href={href}>{children}</a>
}
// AppLink href="/blog"  ✓
// AppLink href="/inexistant"  ✗ TypeScript error

#Variadic tuple types

Les variadic tuple types permettent de typer des fonctions qui opèrent sur des tuples de longueur variable, ce qui est utile pour les middlewares, les pipelines de transformation et les hooks composites.

// Pipeline de transformation type-safe
type Awaited<T> = T extends Promise<infer U> ? U : T

function pipe<A, B>(a: A, fn1: (a: A) => B): B
function pipe<A, B, C>(a: A, fn1: (a: A) => B, fn2: (b: B) => C): C
function pipe<A, B, C, D>(a: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): D
function pipe(a: unknown, ...fns: Array<(x: unknown) => unknown>): unknown {
  return fns.reduce((acc, fn) => fn(acc), a)
}

// Usage : TypeScript infère le type de sortie correctement
const result = pipe(
  { name: 'Nehos', revenue: 1500000 },
  (company) => ({ ...company, slug: company.name.toLowerCase() }),
  (company) => ({ ...company, tier: company.revenue > 1000000 ? 'enterprise' : 'smb' })
)
// type: { name: string; revenue: number; slug: string; tier: 'enterprise' | 'smb' }

#Types utilitaires avancés : Awaited, NoInfer, les patterns indispensables

#NoInfer : bloquer les inférences non souhaitées

NoInfer<T>, introduit dans TypeScript 5.4, empêche TypeScript d'inférer un type depuis un certain paramètre, forçant l'inférence à partir d'une autre source.

// PROBLÈME : TypeScript élargit le type depuis la valeur par défaut
function createStore<T>(initialState: T, defaultValue: T): Store<T> {
  // ...
}

const store = createStore({ status: 'active' as const }, { status: 'inactive' })
// Inféré comme { status: string } — perd le type littéral !'active' | 'inactive'

// SOLUTION avec NoInfer
function createStore<T>(initialState: T, defaultValue: NoInfer<T>): Store<T> {
  // defaultValue ne participe plus à l'inférence de T
}

const store = createStore({ status: 'active' as const }, { status: 'inactive' as const })
// Inféré correctement depuis initialState : { status: 'active' }
// TypeScript vérifie ensuite que defaultValue est assignable à ce type ✓

#Application Next.js : Server Action avec NoInfer

// Factory de Server Action type-safe
function createServerAction<TInput, TOutput>(
  handler: (input: TInput) => Promise<TOutput>,
  errorHandler: (error: unknown) => NoInfer<TOutput> // Force le même type de retour
): (input: TInput) => Promise<TOutput> {
  return async (input) => {
    try {
      return await handler(input)
    } catch (error) {
      return errorHandler(error)
    }
  }
}

#Awaited : typer les promesses imbriquées

Awaited<T> déplie récursivement les promesses, ce qui est indispensable pour typer les données retournées par les Server Components.

// Types utilitaires pour les fetchers Next.js
type FetchResult<T> = Awaited<ReturnType<T extends (...args: unknown[]) => unknown ? T : never>>

async function fetchProjects() {
  const response = await fetch('/api/projects')
  return response.json() as Promise<Project[]>
}

type ProjectsData = FetchResult<typeof fetchProjects>
// type ProjectsData = Project[]  — sans avoir à écrire le type manuellement

// Pattern de cache type-safe avec Awaited
const cache = new Map<string, Awaited<ReturnType<typeof fetchProjects>>>()

#Satisfies : validation sans élargissement de type

Le mot-clé satisfies (TS 4.9, largement adopté avec TS 5) valide qu'une valeur correspond à un type sans changer le type inféré de la valeur.

type Route = {
  path: string
  handler: string
  methods: readonly ('GET' | 'POST' | 'PUT' | 'DELETE')[]
}

// satisfies : valide la structure MAIS préserve les types littéraux
const apiRoutes = [
  { path: '/api/users', handler: 'UserHandler', methods: ['GET', 'POST'] as const },
  { path: '/api/projects', handler: 'ProjectHandler', methods: ['GET', 'POST', 'PUT'] as const },
] satisfies Route[]

// apiRoutes[0].methods est readonly ['GET', 'POST'] — pas juste string[]
// TypeScript détecte aussi les erreurs de typage dans la définition ✓

#Strict mode et configuration tsconfig.json optimale pour Next.js B2B

Le fichier tsconfig.json généré par create-next-app en 2026 active déjà strict: true. Voici la configuration recommandée par l'équipe Nehos pour les projets B2B Next.js App Router, avec les explications de chaque option.

{
  "compilerOptions": {
    // Cibles et modules
    "target": "ES2022",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,

    // Compatibilité
    "allowJs": false,
    "jsx": "preserve",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,

    // Strictness — tous activés pour les projets B2B
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,

    // Performance
    "incremental": true,
    "skipLibCheck": true,
    "isolatedModules": true,

    // Output (géré par Next.js)
    "noEmit": true,

    // Paths (voir section suivante)
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@ui/*": ["./src/components/ui/*"],
      "@lib/*": ["./src/lib/*"],
      "@types/*": ["./src/types/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

#Options critiques pour les projets B2B

noUncheckedIndexedAccess : force TypeScript à inclure undefined dans le type d'un accès indexé (array[0] retourne T | undefined au lieu de T). Élimine une classe entière de bugs runtime.

const users: User[] = await fetchUsers()
const firstUser = users[0]
// Sans noUncheckedIndexedAccess : firstUser est User — accès direct possible
// Avec noUncheckedIndexedAccess : firstUser est User | undefined — vérification obligatoire
if (firstUser) {
  console.log(firstUser.name) // ✓
}

exactOptionalPropertyTypes : distingue { prop?: string } (la propriété peut être absente) de { prop: string | undefined } (la propriété est présente mais peut valoir undefined). Critique pour les APIs qui utilisent les deux conventions.

moduleResolution: "bundler" : introduit dans TypeScript 5.0, ce mode est aligné sur la résolution des bundlers modernes (Vite, Turbopack). Il permet les imports sans extensions et les exports de packages via le champ exports dans package.json, ce qui est nécessaire pour les workspaces Turborepo.


#Path aliases et module resolution : setup complet pour monorepos

Les projets Next.js B2B de taille significative utilisent des monorepos, souvent avec Turborepo. La configuration des path aliases doit être cohérente entre le tsconfig racine, les tsconfigs des packages, et la configuration de Next.js.

#Structure recommandée pour un monorepo Next.js

monorepo/
├── apps/
│   └── web/              # Application Next.js
│       ├── tsconfig.json # Extends tsconfig.base.json
│       └── src/
├── packages/
│   ├── ui/               # Composants partagés
│   │   └── tsconfig.json
│   ├── db/               # Prisma / Drizzle
│   │   └── tsconfig.json
│   └── config/           # tsconfig.base.json, eslint configs
│       └── tsconfig.base.json
└── turbo.json

#tsconfig.base.json (package config)

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "noEmit": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}

#tsconfig.json dans apps/web

{
  "extends": "@repo/config/tsconfig.base.json",
  "compilerOptions": {
    "jsx": "preserve",
    "lib": ["dom", "dom.iterable", "esnext"],
    "plugins": [{ "name": "next" }],
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@ui/*": ["../../packages/ui/src/*"],
      "@db/*": ["../../packages/db/src/*"]
    },
    "incremental": true
  },
  "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

#next.config.ts : synchroniser les paths aliases

Depuis Next.js 15, la configuration peut s'écrire en TypeScript natif :

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    typedRoutes: true // Active les types stricts pour les routes Next.js
  },
  // Les path aliases tsconfig sont automatiquement lus par Next.js/Turbopack
  // Pas besoin de les redéfinir ici si moduleResolution: 'bundler' est actif
}

export default nextConfig

#Typed Routes : les routes Next.js comme types littéraux

import Link from 'next/link'

// Avec typedRoutes: true, href est typé sur les routes existantes
<Link href="/blog">Blog</Link>          // ✓
<Link href="/blog/[slug]">Post</Link>   // ✓ (avec paramètre)
<Link href="/inexistant">404</Link>     // ✗ TypeScript error — la route n'existe pas

#Types pour Server Actions, Server Components et Route Handlers

L'App Router Next.js introduit des primitives pour lesquelles les types TypeScript sont précis mais nécessitent quelques patterns spécifiques.

#Server Components : typage de params et searchParams

Depuis Next.js 15, params et searchParams sont des Promises asynchrones :

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

type PageProps = {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params
  const post = await fetchPost(slug)
  return {
    title: post.title,
    description: post.excerpt
  }
}

export default async function BlogPostPage({ params, searchParams }: PageProps) {
  const { slug } = await params
  const { page = '1' } = await searchParams
  const post = await fetchPost(slug)
  return <ArticleContent post={post} currentPage={Number(page)} />
}

#Server Actions : typer les mutations avec useFormState

// lib/actions/contact.ts
'use server'

import { z } from 'zod'

const ContactSchema = z.object({
  name: z.string().min(2, 'Nom requis (min 2 caractères)'),
  email: z.string().email('Email invalide'),
  message: z.string().min(20, 'Message trop court (min 20 caractères)')
})

export type ContactActionState = {
  success: boolean
  errors?: z.ZodFormattedError<z.infer<typeof ContactSchema>>
  message?: string
}

export async function submitContact(
  prevState: ContactActionState,
  formData: FormData
): Promise<ContactActionState> {
  const raw = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message')
  }

  const parsed = ContactSchema.safeParse(raw)

  if (!parsed.success) {
    return {
      success: false,
      errors: parsed.error.format()
    }
  }

  await sendEmail(parsed.data)
  return { success: true, message: 'Message envoyé avec succès.' }
}
// components/ContactForm.tsx
'use client'

import { useActionState } from 'react'
import { submitContact, type ContactActionState } from '@/lib/actions/contact'

const initialState: ContactActionState = { success: false }

export function ContactForm() {
  const [state, action, isPending] = useActionState(submitContact, initialState)

  return (
    <form action={action}>
      <input name="name" />
      {state.errors?.name && <p>{state.errors.name._errors[0]}</p>}
      <input name="email" />
      {state.errors?.email && <p>{state.errors.email._errors[0]}</p>}
      <textarea name="message" />
      {state.errors?.message && <p>{state.errors.message._errors[0]}</p>}
      <button disabled={isPending}>{isPending ? 'Envoi...' : 'Envoyer'}</button>
      {state.success && <p>{state.message}</p>}
    </form>
  )
}

#Route Handlers : typer Request, Response et les body

// app/api/projects/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

const CreateProjectSchema = z.object({
  name: z.string().min(3),
  budget: z.number().positive(),
  clientId: z.string().uuid()
})

type CreateProjectBody = z.infer<typeof CreateProjectSchema>

export async function POST(request: NextRequest): Promise<NextResponse> {
  const body: unknown = await request.json()

  const parsed = CreateProjectSchema.safeParse(body)
  if (!parsed.success) {
    return NextResponse.json(
      { error: 'Validation failed', details: parsed.error.flatten() },
      { status: 400 }
    )
  }

  const project = await db.project.create({ data: parsed.data })
  return NextResponse.json(project, { status: 201 })
}

export async function GET(request: NextRequest): Promise<NextResponse> {
  const searchParams = request.nextUrl.searchParams
  const clientId = searchParams.get('clientId')

  const projects = await db.project.findMany({
    where: clientId ? { clientId } : undefined
  })

  return NextResponse.json(projects)
}

#TypeScript et Zod : validation runtime + types statiques, le pattern gagnant

Zod est devenu la librairie de validation de référence dans l'écosystème Next.js TypeScript. Son principe fondamental : le schéma Zod est la source unique de vérité pour le type TypeScript ET la validation à l'exécution.

#Le problème que Zod résout

Sans Zod, les équipes doublonnent systématiquement leurs définitions :

// SANS Zod — duplication obligatoire
type User = {
  id: string
  email: string
  role: 'admin' | 'user'
  createdAt: Date
}

// Validation manuelle (fragile, désynchronisée du type)
function validateUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    typeof (data as User).id === 'string' &&
    typeof (data as User).email === 'string' // etc.
  )
}

#Le pattern Zod : source unique de vérité

// AVEC Zod — source unique de vérité
import { z } from 'zod'

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
  createdAt: z.coerce.date()
})

// Type inféré automatiquement depuis le schéma
type User = z.infer<typeof UserSchema>

// Validation à l'exécution — même source
const result = UserSchema.safeParse(apiResponse)
if (result.success) {
  const user: User = result.data // TypeScript et runtime sont en accord
}

#Schémas Zod partagés entre frontend et backend

Dans un monorepo, les schémas Zod vivent dans un package partagé :

// packages/schemas/src/project.ts
import { z } from 'zod'

export const ProjectStatusSchema = z.enum(['draft', 'active', 'completed', 'archived'])

export const ProjectSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(3).max(100),
  status: ProjectStatusSchema,
  budget: z.number().positive(),
  clientId: z.string().uuid(),
  tags: z.array(z.string()).max(10),
  metadata: z.record(z.string(), z.unknown()).optional(),
  createdAt: z.coerce.date(),
  updatedAt: z.coerce.date()
})

export const CreateProjectSchema = ProjectSchema.omit({
  id: true,
  createdAt: true,
  updatedAt: true
})

export const UpdateProjectSchema = CreateProjectSchema.partial()

// Types inférés
export type Project = z.infer<typeof ProjectSchema>
export type CreateProjectInput = z.infer<typeof CreateProjectSchema>
export type UpdateProjectInput = z.infer<typeof UpdateProjectSchema>
export type ProjectStatus = z.infer<typeof ProjectStatusSchema>

#Integration avec tRPC pour des API 100% type-safe

Zod + tRPC est la combinaison qui offre la sécurité de type maximale sur les projets Next.js B2B. Les types se propagent automatiquement de l'API vers le client sans génération de code.

// server/routers/project.ts
import { z } from 'zod'
import { router, protectedProcedure } from '../trpc'
import { CreateProjectSchema, UpdateProjectSchema } from '@repo/schemas'

export const projectRouter = router({
  list: protectedProcedure
    .input(z.object({ clientId: z.string().uuid().optional() }))
    .query(async ({ ctx, input }) => {
      return ctx.db.project.findMany({
        where: input.clientId ? { clientId: input.clientId } : undefined
      })
    }),

  create: protectedProcedure
    .input(CreateProjectSchema)
    .mutation(async ({ ctx, input }) => {
      return ctx.db.project.create({ data: input })
    }),

  update: protectedProcedure
    .input(z.object({ id: z.string().uuid(), data: UpdateProjectSchema }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.project.update({
        where: { id: input.id },
        data: input.data
      })
    })
})

#Performance du type-checker : tsc --incremental, skipLibCheck, isolatedModules

Sur les projets Next.js B2B de taille importante (50+ fichiers TypeScript, monorepo multi-packages), les temps de vérification TypeScript peuvent devenir un frein significatif. Voici les leviers pour les optimiser sans sacrifier la sécurité.

#tsc --incremental : cache des vérifications

L'option incremental: true dans tsconfig (ou --incremental en CLI) demande à TypeScript de sauvegarder les informations de compilation dans un fichier .tsbuildinfo. Les vérifications suivantes ne re-analysent que les fichiers modifiés et leurs dépendances.

{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": ".next/cache/tsbuildinfo"
  }
}

Gain typique : 60 à 80 % de réduction du temps de tsc sur les runs consécutifs (CI inclus, si le cache .tsbuildinfo est conservé entre les runs).

# Premier run (cold)
$ tsc --noEmit
Done in 18.4s

# Run suivant avec incremental (3 fichiers modifiés)
$ tsc --noEmit
Done in 3.1s  # -83%

#skipLibCheck : cibler les declarations externes

skipLibCheck: true désactive la vérification des fichiers .d.ts dans node_modules. Sur un projet avec des centaines de dépendances, cela représente des milliers de fichiers de déclaration.

Attention : skipLibCheck ne désactive PAS la vérification de votre code ni des types de vos dépendances tels qu'utilisés dans votre code. Il désactive uniquement la vérification de la cohérence interne des fichiers .d.ts des librairies tierces.

# Mesure de l'impact de skipLibCheck
$ tsc --noEmit --diagnostics 2>&1 | grep "Check time"
# Avec skipLibCheck: false
Check time:                 14.02s
# Avec skipLibCheck: true
Check time:                  6.81s  # -51%

#isolatedModules : alignement avec Turbopack/SWC

isolatedModules: true demande à TypeScript de signaler les patterns qui ne peuvent pas être transpilés fichier par fichier. C'est obligatoire avec SWC (le transpileur de Next.js) et Turbopack, qui traitent chaque fichier isolément.

// ERREUR avec isolatedModules: true
export { SomeType } from './types' // TypeScript ne sait pas si c'est un type ou une valeur

// CORRECTION : import/export de types explicite
export type { SomeType } from './types' // ✓ explicitement un type

#Project references : parallélisation dans les monorepos

Les project references TypeScript permettent de compiler les packages d'un monorepo en parallèle et de partager le cache tsbuildinfo entre packages.

// tsconfig.json à la racine du monorepo
{
  "files": [],
  "references": [
    { "path": "./packages/schemas" },
    { "path": "./packages/ui" },
    { "path": "./packages/db" },
    { "path": "./apps/web" }
  ]
}
# Build en parallèle avec project references
$ tsc --build --parallel
# TypeScript analyse les dépendances et parallélise les packages indépendants

#Synthèse des gains de performance

OptimisationGain typiqueImpact sur la sécurité
incremental: true-60 à -80 % (runs consécutifs)Aucun
skipLibCheck: true-40 à -55 %Minimal (code tiers uniquement)
isolatedModules: trueN/A (obligation SWC)Améliore la détection de patterns dangereux
Project references-30 à -50 % (monorepo)Aucun
noEmit: true-10 à -20 % (vérification seule)Aucun

Combinées, ces optimisations permettent de passer d'un tsc --noEmit de 25 secondes à moins de 5 secondes sur un monorepo de taille moyenne — ce qui change radicalement l'expérience développeur en CI et en pre-commit hook.

Questions & Réponses

Questions fréquentes sur TypeScript 5 avec Next.js

La migration est recommandée pour les nouveaux projets, mais pas urgente pour les projets existants fonctionnels. Les deux systèmes sont incompatibles dans le même fichier TypeScript, mais pas dans le même projet. Vous pouvez migrer fichier par fichier. La principale raison de migrer : les anciens decorators (Stage 1) ne sont plus maintenus activement et ne font pas partie du standard ECMAScript. Les librairies modernes adoptent progressivement la nouvelle syntaxe. Pour les projets qui utilisent NestJS ou TypeORM qui dépendent encore d'experimentalDecorators, attendez que ces librairies migrent officiellement avant de changer.
strict: true active un ensemble de 8 options de vérification stricte (strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitAny, noImplicitThis, alwaysStrict, useUnknownInCatchVariables). noUncheckedIndexedAccess et exactOptionalPropertyTypes ne sont PAS inclus dans strict: true car ils introduisent des erreurs sur des patterns très courants — ils sont considérés comme trop stricts pour être activés par défaut. Nous les recommandons néanmoins pour les projets B2B car ils éliminent deux classes importantes de bugs runtime liés aux tableaux et aux objets partiels.
Les interfaces TypeScript n'existent qu'à la compilation : elles sont effacées dans le JavaScript produit et ne peuvent pas valider des données à l'exécution. Quand vous recevez une réponse d'API, TypeScript ne peut pas vérifier que les données correspondent au type — il se contente de vous faire confiance. Zod ajoute une couche de validation à l'exécution, ce qui signifie que si l'API renvoie un champ manquant ou du mauvais type, votre code le détecte immédiatement avec un message d'erreur clair plutôt qu'un comportement imprévisible plus loin dans l'exécution. La combinaison des deux : Zod génère le type TypeScript automatiquement depuis le schéma, ce qui élimine la duplication.
Depuis Next.js 15, params et searchParams sont des Promises asynchrones, pas des objets synchrones. Le type correct est : type PageProps = { params: Promise<{ slug: string }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }> }. Vous devez les await au début de votre Server Component ou de generateMetadata. C'est un changement breaking par rapport à Next.js 14 où params était synchrone. L'erreur TypeScript la plus fréquente lors de la mise à jour : accéder à params.slug directement sans await, ce qui retourne un objet Promise au lieu de la valeur.
La structure recommandée est un tsconfig.base.json dans un package config partagé, avec les options communes (strict, moduleResolution: bundler, isolatedModules, etc.), puis chaque app et package extends cette base et ajoute ses options spécifiques. Les path aliases doivent pointer vers les packages du monorepo avec des chemins relatifs depuis le répertoire de l'app. Activez incremental: true dans l'app Next.js et configurez tsBuildInfoFile vers .next/cache/ pour que Next.js gère le cache. Utilisez les project references TypeScript (tsc --build) pour les vérifications de type en CI — cela parallélise la vérification des packages indépendants.
NoInfer<T> empêche TypeScript d'élargir le type inféré en incluant les paramètres annotés avec NoInfer dans le processus d'inférence. C'est utile quand vous voulez que l'inférence de T soit déterminée uniquement par certains paramètres, et que d'autres paramètres soient simplement vérifiés comme assignables à T sans influencer T. Un exemple classique : createStore<T>(initialState: T, defaultValue: NoInfer<T>). Sans NoInfer, si initialState et defaultValue ont des types différents, TypeScript élargit T pour accommoder les deux. Avec NoInfer, T est inféré depuis initialState uniquement, et TypeScript vérifie séparément que defaultValue est assignable à ce T précis. Résultat : vous attrapez des erreurs de type qui passaient silencieusement auparavant.
Oui, c'est même recommandé et c'est la configuration par défaut dans create-next-app. skipLibCheck désactive uniquement la vérification de la cohérence interne des fichiers .d.ts dans node_modules — TypeScript vérifie toujours que vous utilisez correctement les APIs des librairies tierces dans votre propre code. Le risque théorique : une librairie avec des types .d.ts incohérents en interne pourrait masquer un problème. En pratique, les librairies populaires publient des types corrects, et quand elles ne le font pas, skipLibCheck: false génère des erreurs dans node_modules que vous ne pouvez de toute façon pas corriger. L'option complémentaire utile : noUncheckedIndexedAccess: true, qui elle s'applique à votre code et aux usages des APIs tierces dans votre code.
Réserver un audit