Aller au contenu principal
SEO

Next.js TypeScript : Configuration, App Router et Routes Typées

OS
Orbessia Studio
7 juin 20264115 mots
Next.js TypeScript : Configuration, App Router et Routes Typées

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.


Réponse rapide : Pourquoi activer les routes typées dans Next.js ?
  • 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 configuration next.config.ts.
  • Utilisation : Utilisez le composant Link standard 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.json pré-configuré

  • next-env.d.ts pour les types Next.js

  • Les dépendances typescript, @types/react, @types/node installées

  • Les fichiers en .ts et .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 :

  1. Renommez un fichier en .ts ou .tsx (commencez par next.config.ts ou une page simple)

  2. Lancez next dev

  3. Next.js détecte le changement, installe automatiquement typescript et @types/react, et génère tsconfig.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 :

  • unknown pour 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 Zod

  • satisfies pour 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: true dans tsconfig.json

  • [ ] Plugin Next.js activé (plugins: [{ "name": "next" }])

  • [ ] .next/types/**/*.ts inclus dans include

  • [ ] Pas de any généralisé (chercher avec grep -r ": any" src/)

  • [ ] ignoreBuildErrors désactivé (ou contrôle tsc séparé en CI)

  • [ ] Variables d’environnement validées au démarrage (Zod ou équivalent)

  • [ ] Données externes validées à runtime (formulaires, API, CMS, webhooks)

  • [ ] params et searchParams typés comme Promise<> (Next.js 15+)

  • [ ] Server/Client Components séparés proprement

  • [ ] Metadata API utilisée pour les pages SEO

  • [ ] next build passe sans erreur en CI avant déploiement

  • [ ] Core Web Vitals surveillées (Lighthouse CI, web-vitals)


À retenir pour vos développements Next.js :
  1. Le typage statique élimine les erreurs en cours d'exécution et fiabilise vos maillages internes pour le SEO.
  2. L'activation est expérimentale mais stable pour les architectures de production modernes.
  3. 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.