En bref
Next.js intègre TypeScript nativement depuis plusieurs versions. Avec l’App Router (Next.js 13+), le support s’est considérablement renforcé : plugin TypeScript dédié, types générés automatiquement, routes typées, params et searchParams asynchrones en Next.js 15. Ce guide couvre la configuration initiale, les options tsconfig.json recommandées, le typage des pages et layouts App Router, les routes typées, la validation runtime, et les erreurs les plus fréquentes que les développeurs rencontrent en production.
- Sécurité : Le typage des routes empêche les liens cassés et les fautes de frappe sur les URL en levant une erreur dès la compilation.
- Activation : Ajoutez
experimental: { typedRoutes: true }dans votre configurationnext.config.ts. - Utilisation : Utilisez le composant
Linkstandard de Next.js qui bénéficie automatiquement de l'autocomplétion des chemins.
Next.js et TypeScript forment aujourd’hui le binôme par défaut pour les projets React professionnels. Quand vous lancez create-next-app, la première question est “Would you like to use TypeScript?”, et la réponse dans l’écosystème professionnel est quasi systématiquement oui.
Pourtant, entre le setup initial et un projet App Router correctement typé, il y a un écart que beaucoup de développeurs sous-estiment. Les erreurs PageProps, les params qui deviennent des Promises en Next.js 15, les any qui s’accumulent dans les Server Components, les routes dynamiques qui échappent au typage : ces problèmes concrets reviennent constamment sur Stack Overflow et Reddit.
Cet article couvre tout ce qu’il faut savoir pour configurer, typer et maintenir un projet Next.js TypeScript proprement, de tsconfig.json aux patterns App Router avancés.
Configuration initiale
Nouveau projet
La méthode la plus directe :
npx create-next-app@latest my-app --typescript
Cette commande génère un projet avec :
tsconfig.jsonpré-configurénext-env.d.tspour les types Next.jsLes dépendances
typescript,@types/react,@types/nodeinstalléesLes fichiers en
.tset.tsx
Depuis Next.js 14, TypeScript est proposé par défaut dans le CLI interactif. Vous pouvez aussi passer tous les flags directement :
npx create-next-app@latest my-app --ts --app --tailwind --eslint --src-dir --import-alias "@/*"
Migration d’un projet JavaScript existant
Pour un projet Next.js existant en JavaScript, la migration est incrémentale :
Renommez un fichier en
.tsou.tsx(commencez parnext.config.tsou une page simple)Lancez
next devNext.js détecte le changement, installe automatiquement
typescriptet@types/react, et génèretsconfig.json
Vous n’êtes pas obligé de tout convertir d’un coup. Les fichiers .js et .tsx coexistent sans problème. Des praticiens sur Reddit recommandent cette approche progressive : commencer par les fichiers de configuration et les composants partagés, puis convertir page par page.
Le fichier next-env.d.ts
Ce fichier est généré automatiquement à la racine du projet :
/// <reference types="next" />
/// <reference types="next/image-types/global" />
Il permet à TypeScript de reconnaître les types globaux de Next.js (modules d’images, variables d’environnement, etc.). Ne le modifiez pas manuellement. Ajoutez-le à .gitignore si vous voulez, mais Next.js le recréera à chaque next dev ou next build.
Configuration tsconfig.json recommandée
Next.js génère un tsconfig.json fonctionnel, mais comprendre les options clés permet d’éviter des problèmes en aval. Voici une configuration complète commentée pour un projet App Router :
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{ "name": "next" }
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Options critiques
strict: true — Active l’ensemble des vérifications strictes : strictNullChecks, strictFunctionTypes, strictBindCallApply, noImplicitAny, noImplicitThis, alwaysStrict. La documentation TypeScript recommande cette option pour tout nouveau projet. Sans elle, vous perdez une grande partie de l’intérêt de TypeScript. Les undefined passent inaperçus, les paramètres implicitement any ne sont pas signalés, et le compilateur devient trop permissif.
moduleResolution: "bundler" — Introduit dans TypeScript 5.0, ce mode correspond au comportement réel des bundlers comme webpack et Turbopack (utilisés par Next.js). Il remplace "node" pour les projets modernes.
plugins: [{ "name": "next" }] — Active le plugin TypeScript de Next.js. Ce plugin vérifie l’usage correct de 'use client' et 'use server', détecte les hooks React dans les Server Components, et valide les options de segment (dynamic, revalidate, etc.) directement dans l’éditeur.
paths — Les alias de chemins évitent les imports relatifs profonds. @/components/Button est plus lisible et plus stable que ../../../components/Button. Next.js résout ces alias automatiquement sans configuration webpack supplémentaire.
.next/types/**/*.ts dans include — Nécessaire pour que TypeScript reconnaisse les types générés par Next.js, notamment les types de routes et les définitions automatiques de PageProps.
L’erreur classique : noEmit
noEmit: true surprend parfois. TypeScript ne compile pas le code dans un projet Next.js, c’est le bundler (SWC/Turbopack) qui s’en charge. TypeScript ne sert qu’à la vérification de types. C’est pourquoi noEmit est activé.
Typage des pages et layouts App Router
C’est le domaine où les erreurs sont les plus fréquentes, surtout pour les développeurs qui passent du Pages Router à l’App Router ou qui suivent des tutoriels datés.
Pages simples
Une page App Router sans paramètres dynamiques :
// app/about/page.tsx
export default function AboutPage() {
return (
<main>
<h1>À propos</h1>
</main>
)
}
Pas besoin de typer quoi que ce soit ici. Le composant est un Server Component par défaut, il ne reçoit pas de props.
Pages avec params (Next.js 15+)
C’est le piège le plus courant. En Next.js 15, params et searchParams sont devenus asynchrones. La signature qui fonctionnait en Next.js 14 provoque maintenant une erreur de type.
Avant (Next.js 14, ne fonctionne plus en 15) :
// ❌ Erreur : Type '{ params: { slug: string } }' does not satisfy 'PageProps'
export default function ServicePage({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>
}
Après (Next.js 15+) :
// app/services/[slug]/page.tsx
type Props = {
params: Promise<{ slug: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
export default async function ServicePage({ params, searchParams }: Props) {
const { slug } = await params
const { ref } = await searchParams
return (
<main>
<h1>Service : {slug}</h1>
{ref && <p>Référence : {ref}</p>}
</main>
)
}
Sur Stack Overflow, cette erreur PageProps génère des dizaines de questions chaque semaine. La cause est presque toujours un copier-coller depuis un tutoriel Next.js 13/14 ou Pages Router. Des développeurs sur Reddit recommandent d’utiliser le type helper PageProps si disponible :
import type { PageProps } from './$types' // généré par Next.js
Mais dans la pratique, le typage manuel avec Promise<> reste l’approche la plus explicite et portable.
Layouts
Les layouts suivent le même pattern :
// app/services/layout.tsx
type Props = {
children: React.ReactNode
params: Promise<{ slug: string }>
}
export default async function ServiceLayout({ children, params }: Props) {
const { slug } = await params
return (
<section>
<nav>Service actif : {slug}</nav>
{children}
</section>
)
}
generateMetadata typé
La Metadata API bénéficie fortement du typage. Voici un exemple complet :
// app/services/[slug]/page.tsx
import type { Metadata } from 'next'
type Props = {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const service = await getService(slug)
return {
title: service.title,
description: service.description,
openGraph: {
title: service.title,
description: service.description,
type: 'website',
},
}
}
export default async function ServicePage({ params }: Props) {
const { slug } = await params
const service = await getService(slug)
return (
<main>
<h1>{service.title}</h1>
<p>{service.description}</p>
</main>
)
}
Le type Metadata de Next.js est exhaustif. L’autocomplétion dans VS Code montre toutes les propriétés disponibles (robots, alternates, icons, verification, etc.), ce qui évite les erreurs dans les métadonnées SEO critiques. La documentation Next.js précise que les métadonnées sont résolues côté serveur et incluses dans le HTML initial, exactement ce que Google attend pour l’indexation.
Routes typées (typedRoutes)
Next.js propose une fonctionnalité expérimentale (stabilisée en 15.5) qui génère des définitions de types pour toutes les routes du projet.
Activation
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
typedRoutes: true,
},
}
export default nextConfig
Utilisation
Une fois activée, les composants <Link> et les appels router.push() bénéficient de la vérification de types :
import Link from 'next/link'
// ✅ Compile si la route existe
<Link href="/services">Services</Link>
// ❌ Erreur TypeScript si la route n'existe pas
<Link href="/srevices">Services</Link>
Limites en pratique
Les routes construites dynamiquement posent problème. Des développeurs sur GitHub signalent que router.push() avec une chaîne calculée demande un cast explicite :
import type { Route } from 'next'
const path = `/services/${slug}` as Route
router.push(path)
Ce n’est pas idéal. Les routes typées fonctionnent mieux pour les liens statiques dans les templates que pour la navigation dynamique. Pour les projets avec beaucoup de routes dynamiques, des bibliothèques comme next-typesafe-url complètent cette fonctionnalité.
Server Components vs Client Components : le typage
L’App Router distingue les Server Components (par défaut) et les Client Components (marqués 'use client'). TypeScript aide à maintenir cette frontière.
Server Components
Par défaut, tout composant dans app/ est un Server Component. Il peut être async, accéder directement à la base de données, lire des fichiers, appeler des API sans exposer de secrets côté client.
// app/services/page.tsx (Server Component)
import { db } from '@/lib/db'
export default async function ServicesPage() {
const services = await db.service.findMany()
return (
<ul>
{services.map((service) => (
<li key={service.id}>{service.title}</li>
))}
</ul>
)
}
TypeScript vérifie que services a le bon type et que service.title existe. Pas de useEffect, pas de fetch client, pas de state.
Client Components
Les composants qui utilisent des hooks React (useState, useEffect, useRef), des event handlers, ou des API navigateur doivent être marqués 'use client' :
'use client'
import { useState } from 'react'
type ContactFormProps = {
serviceSlug: string
serviceName: string
}
export function ContactForm({ serviceSlug, serviceName }: ContactFormProps) {
const [email, setEmail] = useState('')
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setStatus('sending')
const res = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify({ email, serviceSlug }),
})
setStatus(res.ok ? 'sent' : 'error')
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button type="submit" disabled={status === 'sending'}>
Contacter pour {serviceName}
</button>
{status === 'sent' && <p>Message envoyé.</p>}
{status === 'error' && <p>Erreur, réessayez.</p>}
</form>
)
}
Le plugin TypeScript de Next.js signale dans l’éditeur si vous utilisez useState dans un fichier sans 'use client'. C’est une protection utile qui évite des erreurs runtime obscures.
Pattern : séparer les données du rendu interactif
La bonne pratique consiste à garder les Server Components pour le data fetching et passer les données aux Client Components via les props :
// app/services/[slug]/page.tsx (Server Component)
import { getService } from '@/lib/services'
import { ContactForm } from '@/components/ContactForm'
export default async function ServicePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const service = await getService(slug)
return (
<main>
<h1>{service.title}</h1>
<p>{service.description}</p>
<ContactForm serviceSlug={slug} serviceName={service.title} />
</main>
)
}
TypeScript vérifie que les props passées au ContactForm correspondent au type attendu. Si getService renvoie un objet sans title, l’erreur apparaît à la compilation.
Typage des Route Handlers (API Routes App Router)
Les Route Handlers remplacent les API Routes du Pages Router. Voici comment les typer correctement :
// app/api/services/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { db } from '@/lib/db'
const createServiceSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().min(10),
slug: z.string().regex(/^[a-z0-9-]+$/),
})
export async function POST(request: NextRequest) {
const body = await request.json()
const result = createServiceSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{ error: result.error.flatten() },
{ status: 400 }
)
}
const service = await db.service.create({ data: result.data })
return NextResponse.json(service, { status: 201 })
}
export async function GET() {
const services = await db.service.findMany({
orderBy: { createdAt: 'desc' },
})
return NextResponse.json(services)
}
Point important : NextRequest et NextResponse sont les types fournis par Next.js pour les Route Handlers. Ils étendent les API Web standard (Request, Response) avec des helpers supplémentaires (nextUrl, cookies, etc.).
Validation runtime avec Zod
TypeScript vérifie les types à la compilation. Mais les types disparaissent à l’exécution. Les données provenant de formulaires, d’API externes, de webhooks Stripe ou d’un CMS headless n’ont aucune garantie de type à runtime.
C’est là que Zod (ou des alternatives comme Valibot, ArkType) entre en jeu :
import { z } from 'zod'
// Schéma de validation
const serviceSchema = z.object({
id: z.string().uuid(),
title: z.string(),
description: z.string(),
price: z.number().positive().optional(),
status: z.enum(['draft', 'published', 'archived']),
})
// Type inféré depuis le schéma (pas de duplication)
type Service = z.infer<typeof serviceSchema>
// Utilisation
async function fetchService(slug: string): Promise<Service> {
const res = await fetch(`https://api.example.com/services/${slug}`)
const data = await res.json()
// Valide ET type en une seule opération
return serviceSchema.parse(data)
}
L’avantage de z.infer est l’absence de duplication. Le type TypeScript et le schéma de validation dérivent de la même source. Si le schéma change, le type suit automatiquement.
Des praticiens sur Reddit insistent sur ce point : un projet qui type tout avec TypeScript mais ne valide jamais les données entrantes a un faux sentiment de sécurité. TypeScript protège le code interne, Zod protège les frontières du système.
Structuration d’un projet App Router typé
Les grands projets App Router deviennent rapidement désorganisés. Sur Reddit, plusieurs développeurs recommandent de garder le dossier app/ mince et de déplacer la logique dans des modules structurés. Voici un pattern éprouvé :
src/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── services/
│ │ ├── [slug]/
│ │ │ └── page.tsx
│ │ └── page.tsx
│ └── api/
│ └── contact/
│ └── route.ts
├── components/
│ ├── ui/ # Composants génériques (Button, Card, Input)
│ └── services/ # Composants liés au domaine services
│ ├── ServiceCard.tsx
│ └── ServiceList.tsx
├── lib/
│ ├── db.ts # Instance Prisma
│ ├── utils.ts # Helpers partagés
│ └── env.ts # Variables d'environnement typées
├── features/
│ └── services/
│ ├── types.ts # Types du domaine
│ ├── queries.ts # Data fetching
│ └── actions.ts # Server Actions
└── types/
└── global.d.ts # Types globaux, augmentations
Variables d’environnement typées
Les variables d’environnement sont une source fréquente d’erreurs. Par défaut, process.env.MA_VARIABLE retourne string | undefined, ce qui force des vérifications partout ou des casts dangereux.
Une approche propre :
// src/lib/env.ts
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
NEXT_PUBLIC_SITE_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'production', 'test']),
})
export const env = envSchema.parse(process.env)
Si une variable manque ou a un format incorrect, l’application crashe au démarrage avec un message clair, plutôt que de tomber silencieusement en production sur un undefined.
Server Actions typées
Les Server Actions (Next.js 14+) permettent d’appeler des fonctions serveur directement depuis les Client Components sans passer par un Route Handler. Le typage est essentiel ici :
// src/features/contact/actions.ts
'use server'
import { z } from 'zod'
const contactSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10).max(2000),
})
type ActionResult =
| { success: true; message: string }
| { success: false; errors: Record<string, string[]> }
export async function submitContact(
_prevState: ActionResult | null,
formData: FormData
): Promise<ActionResult> {
const raw = {
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
}
const result = contactSchema.safeParse(raw)
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors as Record<string, string[]>,
}
}
// Envoyer l'email, sauvegarder en base, etc.
await sendContactEmail(result.data)
return { success: true, message: 'Message envoyé avec succès.' }
}
Côté client, avec useActionState (React 19 / Next.js 15) :
'use client'
import { useActionState } from 'react'
import { submitContact } from '@/features/contact/actions'
export function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, null)
return (
<form action={formAction}>
<input name="name" required />
{state?.success === false && state.errors.name && (
<p className="text-red-500">{state.errors.name[0]}</p>
)}
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Envoi...' : 'Envoyer'}
</button>
{state?.success && <p className="text-green-600">{state.message}</p>}
</form>
)
}
Le type discriminé ActionResult (avec success: true | false) permet un narrowing propre dans le template. TypeScript sait que si state.success est false, state.errors existe.
Données structurées JSON-LD typées
Pour le SEO, les données structurées sont du code, et TypeScript peut les sécuriser :
// src/components/JsonLd.tsx
type LocalBusinessJsonLd = {
'@context': 'https://schema.org'
'@type': 'LocalBusiness' | 'ProfessionalService'
name: string
url: string
telephone?: string
address: {
'@type': 'PostalAddress'
streetAddress: string
addressLocality: string
postalCode: string
addressCountry: string
}
geo?: {
'@type': 'GeoCoordinates'
latitude: number
longitude: number
}
}
export function LocalBusinessJsonLd({ data }: { data: LocalBusinessJsonLd }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
)
}
Si vous oubliez addressLocality ou passez un string à latitude, TypeScript le signale. Pour des schémas plus complexes, la bibliothèque schema-dts fournit les types complets de Schema.org.
Erreurs fréquentes et solutions
Type 'X' does not satisfy the constraint 'PageProps'
Cause : en Next.js 15, params et searchParams sont des Promises. Le typage synchrone ne fonctionne plus.
// ❌ Next.js 15
export default function Page({ params }: { params: { id: string } }) {}
// ✅ Next.js 15
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
}
useState dans un Server Component
Le plugin Next.js TypeScript le signale, mais si le plugin n’est pas configuré ou si vous utilisez un éditeur sans LSP, l’erreur apparaîtra seulement au runtime.
// ❌ Manque 'use client' en haut du fichier
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0) // Erreur runtime
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
Abus de any
Des praticiens sur Reddit le répètent : si chaque type complexe est remplacé par any, le projet perd tout l’intérêt de TypeScript. Un projet rempli de any est du JavaScript déguisé avec la complexité syntaxique en plus.
Alternatives à any :
unknownpour les valeurs dont le type n’est pas connu (force une vérification avant utilisation)Les génériques pour les fonctions réutilisables
z.infer<typeof schema>pour les données validées par Zodsatisfiespour vérifier un type sans l’élargir
// ❌
const config: any = getConfig()
// ✅
const config: unknown = getConfig()
if (isValidConfig(config)) {
// config est maintenant typé
}
// ✅ Avec satisfies (TypeScript 4.9+)
const routes = {
home: '/',
services: '/services',
contact: '/contact',
} satisfies Record<string, string>
ignoreBuildErrors: true
L’option existe dans next.config.ts :
const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true, // ⚠️ Dangereux
},
}
La documentation Next.js la qualifie explicitement de dangereuse. Sauf si votre pipeline CI exécute tsc --noEmit séparément avant le build, cette option laisse passer du code potentiellement cassé en production. La seule raison valable : une migration progressive où certains fichiers ne sont pas encore convertis et où un contrôle TypeScript séparé couvre les fichiers critiques.
Types Prisma non synchronisés
Si vous utilisez Prisma, les types sont générés depuis le schéma. Après un changement de schéma, il faut regénérer :
npx prisma generate
Sans cette étape, TypeScript utilise les anciens types et le code compile alors que la base de données a changé.
Next.js TypeScript et performances SEO
TypeScript n’affecte pas directement les performances côté client. Les types sont supprimés à la compilation, le navigateur reçoit du JavaScript classique. Mais l’association Next.js + TypeScript crée des conditions favorables au référencement.
Rendu serveur et statique. Google recommande le rendu serveur ou statique pour les contenus JavaScript importants. Next.js avec l’App Router rend les pages côté serveur par défaut. Le contenu est immédiatement disponible dans le HTML pour les robots d’indexation.
Métadonnées typées. Le type Metadata garantit que les propriétés title, description, openGraph et robots sont correctement structurées. Un title: undefined est détecté à la compilation, pas en production quand Google a déjà indexé une page sans titre.
Core Web Vitals. Les seuils définis (LCP < 2,5s, INP < 200ms, CLS < 0,1) dépendent de l’implémentation, pas du langage. Mais TypeScript aide à maintenir une architecture où les composants lourds sont clairement séparés (Client Components) des composants légers rendus côté serveur.
Pour des projets où le SEO technique est critique (pages locales, données structurées, maillage interne complexe), cette rigueur au niveau du code se traduit par moins d’erreurs dans les éléments qui comptent pour l’indexation. C’est le type de travail qu’implique une optimisation SEO technique sérieuse.
Next.js TypeScript vs alternatives
Stack | Forces | Limites |
|---|---|---|
Next.js + TypeScript | SSR/SSG natif, types stricts, Metadata API, Server Components, écosystème React complet | Courbe d’apprentissage App Router, complexité de configuration pour les débutants |
Next.js + JavaScript | Démarrage plus rapide, moins de friction initiale | Maintenabilité dégradée sur les gros projets, pas de vérification statique |
Remix + TypeScript | Loader/action typés, conventions proches du web, streaming natif | Écosystème plus petit, moins de contenu éducatif disponible |
Nuxt (Vue) + TypeScript | Auto-imports typés, DX Vue, support TypeScript natif | Écosystème Vue vs React, moins de jobs marché |
Astro + TypeScript | Optimal pour le contenu statique, islands architecture | Moins adapté aux applications interactives complexes |
SPA React (Vite) | Setup simple, contrôle total | Pas de SSR intégré, SEO à construire soi-même |
Pour un projet qui combine contenu statique, pages dynamiques, API routes et interactivité, Next.js + TypeScript reste le choix le plus complet. Pour du contenu purement statique, Astro mérite d’être considéré. Pour une SPA sans besoin SEO, Vite + React suffit.
Checklist d’un projet Next.js TypeScript bien configuré
[ ]
strict: truedanstsconfig.json[ ] Plugin Next.js activé (
plugins: [{ "name": "next" }])[ ]
.next/types/**/*.tsinclus dansinclude[ ] Pas de
anygénéralisé (chercher avecgrep -r ": any" src/)[ ]
ignoreBuildErrorsdésactivé (ou contrôletscséparé en CI)[ ] Variables d’environnement validées au démarrage (Zod ou équivalent)
[ ] Données externes validées à runtime (formulaires, API, CMS, webhooks)
[ ]
paramsetsearchParamstypés commePromise<>(Next.js 15+)[ ] Server/Client Components séparés proprement
[ ] Metadata API utilisée pour les pages SEO
[ ]
next buildpasse sans erreur en CI avant déploiement[ ] Core Web Vitals surveillées (Lighthouse CI, web-vitals)
- Le typage statique élimine les erreurs en cours d'exécution et fiabilise vos maillages internes pour le SEO.
- L'activation est expérimentale mais stable pour les architectures de production modernes.
- Combinez TypeScript et App Router pour structurer proprement vos applications métier.
FAQ
Next.js fonctionne-t-il sans TypeScript ?
Oui. Les fichiers .js et .jsx sont supportés. Mais pour un projet professionnel destiné à durer, TypeScript est le choix par défaut recommandé par la documentation Next.js et par la communauté.
TypeScript rend-il le site plus rapide ?
Non. Les types sont supprimés à la compilation. Le JavaScript exécuté dans le navigateur est identique. TypeScript améliore la qualité du code, pas les performances runtime.
Quelle différence entre .ts et .tsx ?
.ts pour le TypeScript sans JSX (utilitaires, configuration, logique métier, route handlers). .tsx pour les fichiers contenant du JSX (composants React, pages, layouts).
Comment migrer un projet Next.js JavaScript vers TypeScript ?
Renommez un fichier en .ts ou .tsx, lancez next dev. Next.js installe les dépendances et génère tsconfig.json. Convertissez fichier par fichier. Les .js et .tsx coexistent.
Pourquoi l’erreur PageProps avec params en Next.js 15 ?
En Next.js 15, params et searchParams sont des Promises. Il faut les typer comme Promise<{ slug: string }> et les await dans le composant. Les tutoriels basés sur Next.js 13/14 utilisent l’ancien format synchrone.
TypeScript remplace-t-il la validation de données ?
Non. TypeScript vérifie les types à la compilation. Les données entrantes (formulaires, API, CMS, webhooks) doivent être validées à l’exécution avec Zod, Valibot ou une solution équivalente.
Les routes typées sont-elles stables ?
La fonctionnalité typedRoutes a été stabilisée en Next.js 15.5. Elle fonctionne bien pour les liens statiques dans les templates. Les routes dynamiques construites par concaténation de strings nécessitent un cast as Route.
Faut-il strict: true ?
Oui. C’est la recommandation officielle de TypeScript et de Next.js. Sans strict, des catégories entières d’erreurs passent silencieusement (valeurs nullables, any implicites, appels de bind incorrects).
Conclusion
Un projet Next.js TypeScript correctement configuré (mode strict, pas de any sauvage, validation runtime aux frontières, Server/Client Components séparés, Metadata API pour le SEO) est un socle solide pour tout projet web professionnel, du site vitrine au SaaS en passant par les applications métier.
La plupart des problèmes que les développeurs rencontrent viennent de trois sources : des tutoriels obsolètes (surtout le passage Pages Router vers App Router et le changement params async en Next.js 15), une configuration tsconfig.json trop permissive, et l’absence de validation runtime pour les données externes.
Si vous travaillez sur un projet Next.js TypeScript et cherchez un accompagnement sur l’architecture, le SEO technique ou le développement, consultez les services d’Orbessia Studio ou prenez contact directement.