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
#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
| Optimisation | Gain typique | Impact sur la sécurité |
|---|---|---|
incremental: true | -60 à -80 % (runs consécutifs) | Aucun |
skipLibCheck: true | -40 à -55 % | Minimal (code tiers uniquement) |
isolatedModules: true | N/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.