TypeScript Strict Mode : Configuration Production-Ready 2026 | Hulli Studio
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 :
noImplicitAny: InterditanyimplicitestrictNullChecks:null/undefineddoivent être gérés explicitementstrictFunctionTypes: Vérification stricte des paramètres de fonctionsstrictBindCallApply: Type-check sur.bind(),.call(),.apply()strictPropertyInitialization: Propriétés de classe initialiséesnoImplicitThis:thisdoit être typé explicitementalwaysStrict:"use strict"dans tous les fichiersuseUnknownInCatchVariables:catch (e)→ typeunknown(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: true ← CRITICAL
// ❌ 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) :
- ✅
noImplicitAny(1 semaine) - ✅
strictBindCallApply(2 jours) - ✅
strictFunctionTypes(3 jours) - ✅
strictPropertyInitialization(1 semaine) - ✅
noImplicitThis(2 jours) - ⚠️
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.jsonavec strict: true -
noUncheckedIndexedAccess: true - ESLint avec plugin TypeScript
- VSCode settings :
"typescript.tsdk": "node_modules/typescript/lib"
✅ Migration
-
noImplicitAny: Tous lesanyexplicites -
strictNullChecks: Tous lesnull/undefinedgé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
TypeErrorruntime : 9/mois- Temps debug moyen : 2.3h/bug
- Confiance refactoring : 60%
Après Strict Mode :
- Bugs production : 2/mois (-86%)
TypeErrorruntime : 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 :
- Bugs : -85% en production
- Refactoring : Confiance totale
- Onboarding : Code autodocumenté
- Maintenance : Coûts réduits de 60%
- 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 :
- ✅ Audit codebase (1-2 jours)
- ✅ Configuration tsconfig optimale
- ✅ Migration progressive avec votre équipe
- ✅ Formation patterns type-safe
- ✅ Support 3 mois post-migration
Demandez un audit TypeScript gratuit →
Articles connexes :
- React Server Components : Le guide complet 2026
- Next.js 15 App Router : Migration complète
- Zod + TypeScript : Validation runtime type-safe
Technologies :