TypeScript Strict Mode : Configuration Production-Ready 2026 | Hulli Studio

Brandon Sueur13 min

Le Strict Mode de TypeScript transform radicalement la robustesse de vos applications React. Mais activer "strict": true sans préparation génère des centaines d'erreurs qui paralysent l'équipe.

Ce guide vous accompagne dans une migration progressive, pragmatique et production-ready vers TypeScript Strict Mode, avec des patterns éprouvés sur nos 40+ projets clients.

Pourquoi Strict Mode est non-négociable en 2026

Les chiffres qui parlent

Étude interne Hulli Studio (40 projets, 2024-2026) :

Métrique Sans Strict Avec Strict Gain
Bugs production 12.3/mois 2.1/mois -83%
Runtime errors 8.7/mois 1.4/mois -84%
Temps debug 18h/mois 4h/mois -78%
Refactoring safety 62% confiance 97% confiance +56%
Onboarding dev 3 semaines 1 semaine -67%

Strict Mode élimine 80%+ des bugs avant qu'ils n'atteignent la production.

Ce que Strict Mode active

// tsconfig.json
{
  "compilerOptions": {
    "strict": true
    // ⬆️ Active automatiquement ces 8 flags :
  }
}

Les 8 flags de strict: true :

  1. noImplicitAny : Interdit any implicite
  2. strictNullChecks : null/undefined doivent être gérés explicitement
  3. strictFunctionTypes : Vérification stricte des paramètres de fonctions
  4. strictBindCallApply : Type-check sur .bind(), .call(), .apply()
  5. strictPropertyInitialization : Propriétés de classe initialisées
  6. noImplicitThis : this doit être typé explicitement
  7. alwaysStrict : "use strict" dans tous les fichiers
  8. useUnknownInCatchVariables : catch (e) → type unknown (TS 4.4+)

Configuration TypeScript Production-Ready

tsconfig.json complet pour React/Next.js

{
  "compilerOptions": {
    /* Type Checking - STRICT */
    "strict": true,
    "noUncheckedIndexedAccess": true, // array[0] peut être undefined
    "noImplicitOverride": true, // Require 'override' keyword
    "noPropertyAccessFromIndexSignature": true,

    /* Modules */
    "module": "ESNext",
    "moduleResolution": "Bundler", // TS 5.0+ (Vite/Next.js)
    "resolveJsonModule": true,
    "allowImportingTsExtensions": false,

    /* Emit */
    "noEmit": true, // Next.js gère la compilation
    "declaration": false,
    "sourceMap": true,

    /* JavaScript Support */
    "allowJs": true, // Migration progressive
    "checkJs": false, // Pas de check sur .js

    /* Interop Constraints */
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,

    /* Language and Environment */
    "target": "ES2022",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "jsx": "preserve",

    /* Completeness */
    "skipLibCheck": true, // Performance (skip node_modules)

    /* Path Mapping */
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/lib/*": ["./src/lib/*"],
      "@/hooks/*": ["./src/hooks/*"],
      "@/types/*": ["./src/types/*"]
    },

    /* Next.js specific */
    "plugins": [
      {
        "name": "next"
      }
    ],
    "incremental": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules", ".next", "out", "dist"]
}

Pourquoi chaque option compte

noUncheckedIndexedAccess: trueCRITICAL

// ❌ Sans noUncheckedIndexedAccess
const users = ['Alice', 'Bob']
const firstUser = users[0] // Type: string (MENSONGE!)
console.log(firstUser.toUpperCase()) // Peut crash si array vide

// ✅ Avec noUncheckedIndexedAccess
const users = ['Alice', 'Bob']
const firstUser = users[0] // Type: string | undefined ✅
console.log(firstUser?.toUpperCase()) // Safe

Impact réel : Élimine 30% des Cannot read property 'X' of undefined en production.

moduleResolution: "Bundler"Next.js 15 / Vite

// ✅ TS 5.0+ avec bundlers modernes
"moduleResolution": "Bundler"

// ❌ Legacy (Node.js pur)
"moduleResolution": "Node"

Supporte les imports modernes : import { Button } from '@/components'

Patterns Type-Safe React

1. Props de composants : Pas de any

// ❌ MAUVAIS : any implicite
export function UserCard({ user }) {
  // ← Type 'any' implicite
  return <div>{user.name}</div>
}

// ⚠️ MOYEN : Inline types
export function UserCard({ user }: { user: { name: string; email: string } }) {
  return <div>{user.name}</div>
}

// ✅ BON : Type dédié
type UserCardProps = {
  user: {
    name: string
    email: string
    avatar?: string // Optional
  }
  onEdit?: (id: string) => void
}

export function UserCard({ user, onEdit }: UserCardProps) {
  return (
    <div>
      <img src={user.avatar} alt={user.name} /> {/* ❌ Error: avatar peut être undefined */}
      <h3>{user.name}</h3>
      <button onClick={() => onEdit?.(user.id)}>Edit</button> {/* ✅ Safe optional call */}
    </div>
  )
}

// ✅ PARFAIT : Fix du avatar
export function UserCard({ user, onEdit }: UserCardProps) {
  return (
    <div>
      {user.avatar && <img src={user.avatar} alt={user.name} />}
      <h3>{user.name}</h3>
    </div>
  )
}

2. useState : Type explicite

// ❌ MAUVAIS : Type inféré trop large
const [user, setUser] = useState(null) // Type: null
setUser({ name: 'Alice' }) // ❌ Error: type incompatible

// ⚠️ MOYEN : Assertion
const [user, setUser] = useState(null as User | null)

// ✅ BON : Generic explicite
type User = {
  id: string
  name: string
  email: string
}

const [user, setUser] = useState<User | null>(null)

// Plus tard...
setUser({ id: '1', name: 'Alice', email: 'alice@example.com' }) // ✅ Type-safe

if (user) {
  console.log(user.name.toUpperCase()) // ✅ TS sait que user n'est pas null
}

3. Event handlers : Types précis

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

// ✅ BON : Type React
import { FormEvent, ChangeEvent } from 'react'

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
  e.preventDefault()
  const formData = new FormData(e.currentTarget)
  // ...
}

const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
  setEmail(e.target.value) // ✅ Type-safe
}

const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
  setCountry(e.target.value)
}

4. useRef : Type du DOM element

// ❌ MAUVAIS
const inputRef = useRef(null) // Type: MutableRefObject<null>
inputRef.current.focus() // ❌ Error: current est null

// ✅ BON : Type explicite
const inputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
  inputRef.current?.focus() // ✅ Safe avec optional chaining
}, [])

// ✅ PARFAIT : Non-null assertion (si certain de l'existence)
const inputRef = useRef<HTMLInputElement>(null!)

useEffect(() => {
  inputRef.current.focus() // ✅ OK car garantie d'existence
}, [])

5. Fetch API : Types de réponse

// ❌ MAUVAIS : any implicite
async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`)
  const user = await res.json() // Type: any
  return user
}

// ✅ BON : Type explicite
type User = {
  id: string
  name: string
  email: string
}

async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`)

  if (!res.ok) {
    throw new Error(`Failed to fetch user: ${res.status}`)
  }

  const user: User = await res.json()
  return user
}

// ✅ PARFAIT : Validation runtime (Zod)
import { z } from 'zod'

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

type User = z.infer<typeof UserSchema>

async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  const data = await res.json()

  // ✅ Validation runtime + type safety
  const user = UserSchema.parse(data)
  return user
}

6. Null checks : strictNullChecks

// ❌ Sans strictNullChecks
function greet(name: string | null) {
  console.log(name.toUpperCase()) // ⚠️ Pas d'erreur TS, mais crash runtime
}

// ✅ Avec strictNullChecks
function greet(name: string | null) {
  console.log(name.toUpperCase()) // ❌ Error: 'name' peut être null
}

// ✅ FIX 1 : Guard clause
function greet(name: string | null) {
  if (!name) return
  console.log(name.toUpperCase()) // ✅ TS sait que name est string
}

// ✅ FIX 2 : Optional chaining
function greet(name: string | null) {
  console.log(name?.toUpperCase() ?? 'Guest')
}

// ✅ FIX 3 : Non-null assertion (si certitude)
function greet(name: string | null) {
  console.log(name!.toUpperCase()) // ⚠️ Use with caution
}

7. Array methods : Type inference

// ❌ MAUVAIS : Perte de type
const users: User[] = [...]
const names = users.map(u => u.name) // Type: any[] (si strict false)

// ✅ BON : Inference automatique
const users: User[] = [...]
const names = users.map(u => u.name) // Type: string[]

// ✅ PARFAIT : Generic explicite
const names = users.map<string>(u => u.name)

// Pattern avancé : Mapping complexe
type UserDTO = {
  id: string
  fullName: string
  emailAddress: string
}

const dtos = users.map<UserDTO>(user => ({
  id: user.id,
  fullName: user.name,
  emailAddress: user.email
}))

Migration progressive vers Strict Mode

Étape 1 : Baseline (Mesurer l'ampleur)

# 1. Activez strict dans tsconfig.json
{
  "compilerOptions": {
    "strict": true
  }
}

# 2. Comptez les erreurs
npx tsc --noEmit | wc -l
# Exemple : 847 errors

# 3. Créez une baseline
npx tsc --noEmit > baseline.txt

Étape 2 : Activer flag par flag (Si trop d'erreurs)

// tsconfig.json - Stratégie progressive
{
  "compilerOptions": {
    "strict": false, // ← Désactivé temporairement

    // Activez 1 par 1
    "noImplicitAny": true // Semaine 1 (plus facile)
    // "strictNullChecks": false,        // Semaine 2 (plus difficile)
    // "strictFunctionTypes": false,     // Semaine 3
    // ...
  }
}

Ordre recommandé (du + facile au + difficile) :

  1. noImplicitAny (1 semaine)
  2. strictBindCallApply (2 jours)
  3. strictFunctionTypes (3 jours)
  4. strictPropertyInitialization (1 semaine)
  5. noImplicitThis (2 jours)
  6. ⚠️ strictNullChecks (2-4 semaines) ← Le plus difficile

Étape 3 : Patterns de fix rapides

Pattern 1 : Type assertion temporaire

// ❌ Erreur après activation de strict
const user = JSON.parse(localStorage.getItem('user')) // any

// ✅ FIX RAPIDE (temporaire)
const user = JSON.parse(localStorage.getItem('user')!) as User

// ✅ FIX IDÉAL (production-ready)
const rawUser = localStorage.getItem('user')
const user = rawUser ? (JSON.parse(rawUser) as User) : null

Pattern 2 : // @ts-expect-error stratégique

// ❌ 300 erreurs de types sur une lib externe
import { OldLibrary } from 'old-library'

// ✅ FIX TEMPORAIRE : Isolez le problème
// @ts-expect-error - Legacy library, will fix in ticket #456
const result = OldLibrary.legacyMethod()

// Later: Create proper types in types/old-library.d.ts

Pattern 3 : Utility types pour accélérer

// Problème : API retourne des champs optionnels
type APIUser = {
  id?: string
  name?: string
  email?: string
}

// ✅ RAPIDE : Required utility
type User = Required<APIUser>
// { id: string; name: string; email: string }

// ✅ RAPIDE : Partial pour updates
type UserUpdate = Partial<User>
// { id?: string; name?: string; email?: string }

// ✅ RAPIDE : Pick pour sous-ensembles
type UserPreview = Pick<User, 'id' | 'name'>
// { id: string; name: string }

// ✅ RAPIDE : Omit pour exclure
type UserWithoutEmail = Omit<User, 'email'>
// { id: string; name: string }

Étape 4 : Script de migration automatisé

# Installer ts-migrate (Airbnb)
npm install -g ts-migrate

# Migrer un dossier
ts-migrate migrate src/components

# Ajoute automatiquement :
# - Types explicites sur paramètres
# - Types sur useState/useRef
# - $TSFixMe sur problèmes complexes

Exemple de transformation :

// AVANT
export function UserCard({ user }) {
  const [loading, setLoading] = useState(false)
  return <div>{user.name}</div>
}

// APRÈS (ts-migrate)
export function UserCard({ user }: $TSFixMe) {
  const [loading, setLoading] = useState<boolean>(false)
  return <div>{user.name}</div>
}

// Puis vous remplacez $TSFixMe par le vrai type
type UserCardProps = { user: User }
export function UserCard({ user }: UserCardProps) {
  //...
}

Patterns avancés Strict Mode

1. Discriminated Unions (Type Guards)

// API qui retourne soit un succès, soit une erreur
type APIResponse<T> = { success: true; data: T } | { success: false; error: string }

async function fetchUser(id: string): Promise<APIResponse<User>> {
  const res = await fetch(`/api/users/${id}`)

  if (res.ok) {
    const data = await res.json()
    return { success: true, data }
  }

  return { success: false, error: `HTTP ${res.status}` }
}

// Utilisation avec narrowing
const response = await fetchUser('123')

if (response.success) {
  console.log(response.data.name) // ✅ TS sait que data existe
} else {
  console.error(response.error) // ✅ TS sait que error existe
}

// ❌ IMPOSSIBLE : TypeScript empêche les bugs
if (response.success) {
  console.log(response.error) // ❌ Error: error n'existe pas ici
}

2. Generic Components Type-Safe

// ✅ Select générique type-safe
type Option<T> = {
  value: T
  label: string
}

type SelectProps<T> = {
  options: Option<T>[]
  value: T
  onChange: (value: T) => void
}

function Select<T extends string | number>({
  options,
  value,
  onChange
}: SelectProps<T>) {
  return (
    <select
      value={value}
      onChange={e => {
        const selectedValue = options.find(
          opt => String(opt.value) === e.target.value
        )?.value

        if (selectedValue !== undefined) {
          onChange(selectedValue)
        }
      }}
    >
      {options.map(opt => (
        <option key={String(opt.value)} value={String(opt.value)}>
          {opt.label}
        </option>
      ))}
    </select>
  )
}

// Utilisation : Types préservés
const countries: Option<string>[] = [
  { value: 'fr', label: 'France' },
  { value: 'us', label: 'USA' }
]

<Select
  options={countries}
  value={selectedCountry} // Type: string
  onChange={setSelectedCountry} // (value: string) => void
/>

3. Branded Types (IDs type-safe)

// Problème : Tous les IDs sont des strings
type UserId = string
type ProductId = string
type OrderId = string

function getUser(id: UserId) {
  /*...*/
}
function getProduct(id: ProductId) {
  /*...*/
}

const userId: UserId = '123'
const productId: ProductId = '456'

getUser(productId) // ⚠️ Pas d'erreur TS, mais logiquement faux

// ✅ SOLUTION : Branded Types
type Brand<K, T> = K & { __brand: T }

type UserId = Brand<string, 'UserId'>
type ProductId = Brand<string, 'ProductId'>

function getUser(id: UserId) {
  /*...*/
}
function getProduct(id: ProductId) {
  /*...*/
}

const userId = '123' as UserId
const productId = '456' as ProductId

getUser(productId) // ❌ Error: Type 'ProductId' n'est pas assignable à 'UserId'
getUser(userId) // ✅ OK

4. Template Literal Types (TS 4.1+)

// Routes type-safe
type Route = '/home' | '/about' | '/blog' | '/contact'
type DynamicRoute = `/blog/${string}` | `/products/${string}`

type AllRoutes = Route | DynamicRoute

function navigate(path: AllRoutes) {
  // ...
}

navigate('/home') // ✅
navigate('/blog/my-article') // ✅
navigate('/products/123') // ✅
navigate('/invalid') // ❌ Error

Erreurs courantes et solutions

1. "Object is possibly 'null'"

// ❌ Erreur
const user = users.find((u) => u.id === '123')
console.log(user.name) // ❌ Object is possibly 'undefined'

// ✅ FIX 1 : Optional chaining
console.log(user?.name)

// ✅ FIX 2 : Guard clause
if (!user) throw new Error('User not found')
console.log(user.name)

// ✅ FIX 3 : Nullish coalescing
console.log(user?.name ?? 'Unknown')

2. "Property 'X' does not exist on type 'never'"

// ❌ Erreur : Union mal gérée
type Response = { success: true; data: User } | { success: false; error: string }

function handle(res: Response) {
  if (res.success) {
    return res.data
  }
  return res.error // ❌ Parfois "never"
}

// ✅ FIX : Type guard explicite
function handle(res: Response) {
  if (res.success) {
    return res.data
  } else {
    return res.error // ✅ TS comprend else
  }
}

3. "Index signature is missing"

// ❌ Erreur
const config = { apiUrl: 'https://api.example.com', timeout: 5000 }
const key = 'apiUrl'
console.log(config[key]) // ❌ Element implicitly has 'any' type

// ✅ FIX 1 : Type assertion
const key = 'apiUrl' as keyof typeof config
console.log(config[key])

// ✅ FIX 2 : Index signature
type Config = {
  [key: string]: string | number
  apiUrl: string
  timeout: number
}

const config: Config = { apiUrl: '...', timeout: 5000 }

Checklist Migration Strict Mode

✅ Préparation

  • Backup du code
  • Tests E2E en place (regression safety)
  • CI/CD configuré avec tsc --noEmit
  • Équipe informée (prévoir 2-4 semaines)

✅ Configuration

  • tsconfig.json avec strict: true
  • noUncheckedIndexedAccess: true
  • ESLint avec plugin TypeScript
  • VSCode settings : "typescript.tsdk": "node_modules/typescript/lib"

✅ Migration

  • noImplicitAny : Tous les any explicites
  • strictNullChecks : Tous les null/undefined gérés
  • Props React : Types explicites
  • Hooks : Generics sur useState/useRef/useReducer
  • Event handlers : Types React
  • API calls : Types de réponse
  • Array indexing : Guards sur array[0]

✅ Validation

  • npm run type-check : 0 errors
  • Tests unitaires : 100% pass
  • Tests E2E : 100% pass
  • Lighthouse : Score maintenu
  • Code review : Approbation équipe

Performance : Avant/Après Strict Mode

Projet SaaS Hulli Studio (TypeScript migration 2025)

Avant Strict Mode :

  • Bugs production : 14/mois
  • TypeError runtime : 9/mois
  • Temps debug moyen : 2.3h/bug
  • Confiance refactoring : 60%

Après Strict Mode :

  • Bugs production : 2/mois (-86%)
  • TypeError runtime : 0/mois (-100%)
  • Temps debug moyen : 0.8h/bug (-65%)
  • Confiance refactoring : 98%

ROI mesuré :

  • Temps dev économisé : 32h/mois
  • Coût bugs évités : ~12 000€/an
  • Vélocité équipe : +40%

Conclusion : Type Safety = Production Safety

En 2026, ne pas utiliser Strict Mode sur un projet TypeScript = malpractice professionnelle.

Pourquoi c'est non-négociable :

  1. Bugs : -85% en production
  2. Refactoring : Confiance totale
  3. Onboarding : Code autodocumenté
  4. Maintenance : Coûts réduits de 60%
  5. Productivité : Moins de debug, plus de features

Chez Hulli Studio, nous avons migré 40+ projets vers Strict Mode avec 100% de succès et des gains mesurables systématiques.


FAQ - TypeScript Strict Mode

Peut-on activer Strict Mode sur un gros projet existant ?

Oui ! Migration progressive flag par flag. Comptez 1-4 semaines selon la taille (1 semaine pour 10k lignes de code en moyenne).

Strict Mode ralentit-il la compilation ?

Non. L'impact est < 5% sur les projets réels. Le gain en productivité compense largement.

Faut-il typer TOUT le code ?

Non. Les libraries bien typées (React, Next.js, Zod) fournissent déjà 80% des types via inference. Vous typez surtout vos props et votre logique métier.

Comment gérer les libraries mal typées ?

Créez des .d.ts dans /types ou utilisez @ts-expect-error localement en attendant une PR upstream.

Vue.js / Angular supportent-ils Strict Mode ?

Oui ! Vue 3 + TypeScript strict fonctionne parfaitement. Angular active strict par défaut depuis v12.


Besoin d'aide pour migrer vers TypeScript Strict ?

Chez Hulli Studio, nous accompagnons les équipes dans la modernisation TypeScript :

  1. ✅ Audit codebase (1-2 jours)
  2. ✅ Configuration tsconfig optimale
  3. ✅ Migration progressive avec votre équipe
  4. ✅ Formation patterns type-safe
  5. ✅ Support 3 mois post-migration

Demandez un audit TypeScript gratuit →


Articles connexes :

Technologies :