Vous développez en JavaScript depuis des années et vous vous demandez si TypeScript vaut vraiment le coup ? La réponse courte : absolument. Mais migrer un projet existant peut sembler intimidant, surtout quand on parle d’une codebase de plusieurs milliers de lignes accumulées au fil des années.

La bonne nouvelle, c’est que TypeScript a été conçu précisément pour faciliter cette transition. Contrairement à un changement de framework qui nécessite une réécriture complète, TypeScript est un sur-ensemble de JavaScript. Votre code JavaScript valide est déjà du TypeScript valide. Vous pouvez migrer fichier par fichier, fonction par fonction, sans tout casser d’un coup.

Cette migration progressive présente des avantages concrets et mesurables : moins de bugs en production, refactoring plus sûr, meilleure documentation implicite du code, et une expérience développeur considérablement améliorée grâce à l’autocomplétion intelligente. Des entreprises comme Airbnb ont rapporté une réduction de 38% des bugs après migration vers TypeScript.

Ce guide vous accompagne étape par étape dans cette transition. Nous commençons par configurer TypeScript dans votre projet existant, puis nous explorons les stratégies de migration pragmatiques qui vous permettront d’adopter TypeScript à votre rythme, sans pression, sans risque.


Pourquoi migrer vers TypeScript en 2025 ?

Avant de plonger dans le « comment », clarifions le « pourquoi ». TypeScript n’est pas juste une mode passagère. En 2025, il s’est imposé comme le standard de facto pour les projets JavaScript professionnels.

Les bénéfices concrets et mesurables

Détection d’erreurs avant l’exécution

Le compilateur TypeScript attrape les erreurs de typage avant même que vous lanciez votre code. Une faute de frappe dans un nom de propriété ? TypeScript vous alerte immédiatement dans votre éditeur. Un argument manquant dans un appel de fonction ? Le compilateur refuse de compiler.

Airbnb a publié une étude montrant que 38% de leurs bugs en production auraient pu être évités avec TypeScript. Cela représente des heures de debugging économisées et une meilleure expérience utilisateur.

Refactoring sûr et rapide

Renommer une propriété utilisée dans 50 fichiers différents ? Avec JavaScript pur, vous priez pour ne rien oublier. Avec TypeScript, vous renommez via votre IDE et tous les usages sont automatiquement mis à jour. Le compilateur vous prévient si un endroit a été oublié.

Cette capacité à refactorer en confiance change radicalement la maintenabilité des grands projets. Vous osez améliorer votre architecture parce que vous savez que TypeScript vous empêchera de casser quoi que ce soit.

Autocomplétion et IntelliSense avancés

Avec JavaScript, votre IDE devine plus ou moins ce que vous essayez de faire. Avec TypeScript, il sait exactement quelles propriétés existent sur un objet, quels arguments une fonction attend, et quel type elle retourne.

Cette amélioration de l’expérience développeur se traduit par une productivité accrue de 15-20% selon plusieurs études. Moins de temps à chercher dans la documentation, moins d’allers-retours entre fichiers pour comprendre une API.

Documentation vivante

Les types TypeScript servent de documentation qui ne peut jamais devenir obsolète. Quand vous modifiez une fonction, vous devez aussi mettre à jour sa signature de type, sinon ça ne compile pas. Fini les commentaires JSDoc qui datent de deux ans et qui ne correspondent plus à la réalité.

typescript

// La signature de type documente exactement ce que fait cette fonction
function calculateDiscount(
  price: number,
  discountPercentage: number,
  maxDiscount?: number
): number {
  // Implémentation
}

Un développeur qui découvre cette fonction comprend immédiatement ce qu’elle attend et ce qu’elle retourne, sans lire l’implémentation ni chercher des exemples d’utilisation.

L’écosystème TypeScript en 2025

Support universel des frameworks

Tous les frameworks modernes supportent TypeScript nativement ou le recommandent activement :

  • React : Create React App, Next.js, Remix, Gatsby
  • Vue : Vue 3 est écrit en TypeScript
  • Angular : TypeScript par défaut depuis toujours
  • Svelte : SvelteKit supporte TypeScript out-of-the-box
  • Node.js : Support natif avec tsx et ts-node

Cette adoption massive signifie que vous trouverez des types pour pratiquement toutes les bibliothèques populaires via DefinitelyTyped.

Demande du marché

Une analyse de 50 000 offres d’emploi développeur JavaScript en 2024 montre que 72% mentionnent TypeScript comme compétence requise ou fortement recommandée. C’est désormais un atout indispensable sur le CV d’un développeur frontend ou fullstack.

Les entreprises qui recrutent savent que TypeScript réduit les bugs et améliore la maintenabilité. Maîtriser TypeScript ouvre des portes vers des projets plus ambitieux et mieux rémunérés.

Quand ne PAS migrer vers TypeScript ?

Soyons honnêtes : TypeScript n’est pas toujours la solution optimale.

Évitez TypeScript si :

  • Vous travaillez sur un prototype ou MVP rapide qui sera probablement jeté dans 2 mois
  • Votre équipe est très junior et découvre encore JavaScript (ajoutez TypeScript dans 6-12 mois)
  • Votre projet fait moins de 1000 lignes et n’évoluera probablement plus
  • Vous développez seul des scripts d’automatisation ponctuels

Migrez vers TypeScript si :

  • Votre codebase dépasse 3000-5000 lignes
  • Plusieurs développeurs travaillent sur le projet
  • Le projet sera maintenu sur plusieurs années
  • Vous passez beaucoup de temps à debugger des erreurs de type
  • Vous voulez faciliter l’onboarding de nouveaux développeurs

Les prérequis avant de commencer

Avant de lancer la migration, assurez-vous d’avoir une base solide.

Maîtrise de JavaScript moderne

TypeScript ajoute des types au-dessus de JavaScript. Si vous n’êtes pas à l’aise avec ES6+ (destructuring, spread operator, arrow functions, promises, async/await), commencez par consolider ces bases.

Revoyez les concepts JavaScript fondamentaux :

Tests automatisés en place

Si votre projet n’a pas de tests, c’est le moment parfait pour en ajouter avant de migrer. Les tests vous serviront de filet de sécurité pendant la migration. Vous pourrez vérifier que votre comportement n’a pas changé après avoir ajouté des types.

Un minimum syndical :

  • Tests unitaires sur la logique métier critique
  • Tests d’intégration sur les flows principaux
  • Tests E2E sur les parcours utilisateurs clés

Configuration Git propre

Créez une branche dédiée pour la migration. Committez fréquemment, fichier par fichier. Si quelque chose casse, vous pourrez facilement revenir en arrière.

bash

git checkout -b migration/typescript

Configurez également un .gitignore pour les fichiers TypeScript générés :

# TypeScript
*.js.map
*.d.ts
dist/
build/

Node.js et npm à jour

TypeScript nécessite Node.js. Assurez-vous d’avoir au minimum Node.js 18+ installé. Vérifiez votre version :

bash

node --version
npm --version

Si nécessaire, mettez à jour via nvm ou téléchargez la dernière version LTS depuis nodejs.org.


Configuration initiale de TypeScript

Installons et configurons TypeScript dans votre projet existant.

Installation de TypeScript

Dans votre projet, installez TypeScript comme dépendance de développement :

bash

npm install --save-dev typescript

Vérifiez que l’installation a réussi :

bash

npx tsc --version

Vous devriez voir quelque chose comme Version 5.3.3 (ou plus récent).

Créer le fichier tsconfig.json

Le tsconfig.json configure le comportement du compilateur TypeScript. Générez un fichier de base :

bash

npx tsc --init

Cette commande crée un tsconfig.json avec beaucoup d’options commentées. Voici une configuration recommandée pour une migration progressive :

json

{
  "compilerOptions": {
    // Cible de compilation : ES2020 fonctionne dans tous les navigateurs modernes
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    
    // Options strictes (à activer progressivement)
    "strict": false,
    "noImplicitAny": false,
    "strictNullChecks": false,
    
    // Permet JavaScript et TypeScript côte à côte
    "allowJs": true,
    "checkJs": false,
    
    // Génération des fichiers
    "outDir": "./dist",
    "rootDir": "./src",
    
    // Support des modules
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    
    // Options de qualité de code
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    
    // Source maps pour le debugging
    "sourceMap": true,
    "declaration": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}

Points clés de cette configuration :

  • allowJs: true : permet de mixer fichiers .js et .ts pendant la migration
  • strict: false : démarre avec TypeScript en mode permissif, on durcira progressivement
  • noImplicitAny: false : autorise le type any implicite au début
  • checkJs: false : ne vérifie pas encore les fichiers JavaScript

Ajuster les scripts npm

Modifiez votre package.json pour ajouter des commandes TypeScript :

json

{
  "scripts": {
    "build": "tsc",
    "build:watch": "tsc --watch",
    "type-check": "tsc --noEmit",
    "dev": "your-existing-dev-command"
  }
}
  • npm run build : compile TypeScript vers JavaScript
  • npm run build:watch : recompile automatiquement à chaque changement
  • npm run type-check : vérifie les types sans générer de fichiers (utile en CI/CD)

Premier test de compilation

Créez un fichier de test src/test.ts :

typescript

const greeting: string = "Hello TypeScript!";
console.log(greeting);

Compilez :

bash

npm run build

Si tout fonctionne, vous devriez voir un fichier dist/test.js généré. Félicitations, TypeScript est opérationnel !


Stratégie de migration progressive

La migration d’un projet entier ne se fait pas en une journée. Voici une stratégie éprouvée pour migrer progressivement sans casser votre production.

Phase 1 : Coexistence JavaScript et TypeScript

Objectif : faire fonctionner TypeScript à côté de votre code JavaScript existant sans rien casser.

  1. Configurez tsconfig.json avec allowJs: true et strict: false
  2. Renommez UN seul fichier simple .js.ts
  3. Corrigez les erreurs TypeScript dans ce fichier
  4. Testez que tout fonctionne encore
  5. Committez

Répétez ce processus fichier par fichier. Commencez par les fichiers utilitaires (utils, helpers) qui ont peu de dépendances. Évitez les gros fichiers complexes au début.

Phase 2 : Migration des fichiers critiques

Objectif : migrer les fichiers qui apportent le plus de valeur quand typés.

Priorisez dans cet ordre :

  1. Modèles de données : vos interfaces/types métier
  2. API clients : fonctions qui font des calls HTTP
  3. Stores/State management : Redux, Zustand, Context API
  4. Composants réutilisables : buttons, inputs, cards
  5. Logique métier : calculs, validations, transformations de données

Ces fichiers bénéficient le plus du typage car ils sont utilisés partout dans l’application.

Phase 3 : Activation progressive du mode strict

Objectif : durcir progressivement les règles TypeScript pour maximiser les bénéfices.

Une fois que 50-70% de votre code est en TypeScript, commencez à activer les options strictes une par une dans tsconfig.json :

  1. Activez noImplicitAny: true → forcez à typer les paramètres de fonctions
  2. Corrigez les erreurs qui apparaissent
  3. Activez strictNullChecks: true → forcez à gérer null/undefined
  4. Corrigez les erreurs
  5. Activez strict: true → active toutes les vérifications strictes
  6. Corrigez les dernières erreurs

Cette approche incrémentale évite d’avoir 500 erreurs d’un coup qui vous découragent.

Phase 4 : Désactivation de allowJs

Objectif : forcer la migration complète.

Quand il ne reste que quelques fichiers JavaScript, désactivez allowJs dans tsconfig.json. Le compilateur refusera maintenant de compiler tant que des fichiers .js existent (hors node_modules).

Cela crée une pression saine pour finaliser la migration. Fixez une deadline réaliste et migrez les derniers fichiers.

Combien de temps prévoir ?

La durée dépend évidemment de la taille de votre projet :

Taille du projetTemps de migration estimé< 5 000 lignes1-2 semaines5 000 - 20 000 lignes1-2 mois20 000 - 50 000 lignes2-4 mois> 50 000 lignes4-12 mois

Ces estimations supposent qu’un développeur consacre 20-30% de son temps à la migration en parallèle des développements features.


Convertir votre premier fichier

Prenons un exemple concret pour voir comment convertir un fichier JavaScript en TypeScript.

Fichier JavaScript original

javascript

// utils/user.js
export function formatUserName(user) {
  if (!user) return "Anonymous";
  return `${user.firstName} ${user.lastName}`.trim();
}

export function isAdminUser(user) {
  return user.role === "admin" || user.permissions.includes("admin");
}

export function getUserAge(user) {
  const today = new Date();
  const birthDate = new Date(user.birthDate);
  let age = today.getFullYear() - birthDate.getFullYear();
  const monthDiff = today.getMonth() - birthDate.getMonth();
  
  if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
    age--;
  }
  
  return age;
}

Étape 1 : Renommer en .ts

Renommez simplement utils/user.jsutils/user.ts.

Lancez la compilation :

bash

npm run type-check

TypeScript va immédiatement identifier des problèmes potentiels.

Étape 2 : Définir les interfaces

Créez un fichier types/user.ts pour vos types :

typescript

// types/user.ts
export interface User {
  id: string;
  firstName: string;
  lastName: string;
  birthDate: string; // ISO date string
  role: "user" | "admin" | "moderator";
  permissions: string[];
}

Étape 3 : Typer les fonctions

Modifiez votre fichier avec les types appropriés :

typescript

// utils/user.ts
import { User } from '../types/user';

export function formatUserName(user: User | null | undefined): string {
  if (!user) return "Anonymous";
  return `${user.firstName} ${user.lastName}`.trim();
}

export function isAdminUser(user: User): boolean {
  return user.role === "admin" || user.permissions.includes("admin");
}

export function getUserAge(user: User): number {
  const today = new Date();
  const birthDate = new Date(user.birthDate);
  let age = today.getFullYear() - birthDate.getFullYear();
  const monthDiff = today.getMonth() - birthDate.getMonth();
  
  if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
    age--;
  }
  
  return age;
}

Étape 4 : Gérer les erreurs TypeScript

TypeScript peut maintenant vous signaler des problèmes que JavaScript ne voyait pas. Par exemple, si ailleurs dans votre code vous appelez :

typescript

formatUserName(null) // OK, géré
isAdminUser(null) // ❌ Erreur : user peut être null

Vous devez corriger l’appelant ou modifier la fonction pour accepter null :

typescript

export function isAdminUser(user: User | null): boolean {
  if (!user) return false;
  return user.role === "admin" || user.permissions.includes("admin");
}

Étape 5 : Tester et valider

Lancez vos tests :

bash

npm test

Si tout passe, committez :

bash

git add src/utils/user.ts src/types/user.ts
git commit -m "Migrer utils/user vers TypeScript"

Félicitations, vous avez converti votre premier fichier ! Répétez ce processus pour les autres fichiers.


Gérer les types dans les dépendances tierces

Une des beautés de TypeScript, c’est que la plupart des bibliothèques populaires fournissent déjà des types.

Bibliothèques avec types intégrés

Certaines librairies sont écrites en TypeScript et incluent leurs types :

  • React (depuis v18)
  • Vue 3
  • Express (via @types/express)
  • Axios
  • date-fns

Quand vous installez ces packages, les types viennent automatiquement. Votre IDE vous offre l’autocomplétion immédiatement.

DefinitelyTyped : types pour les packages JavaScript

Pour les packages JavaScript qui n’incluent pas de types, la communauté maintient DefinitelyTyped, un dépôt centralisé de définitions de types.

Si vous utilisez lodash :

bash

npm install lodash
npm install --save-dev @types/lodash

Le package @types/lodash contient les définitions de types pour lodash. Maintenant TypeScript connaît tous les types de lodash.

Recherchez les types disponibles sur https://www.npmjs.com/search?q=%40types.

Bibliothèques sans types disponibles

Que faire si une bibliothèque n’a pas de types et n’existe pas sur DefinitelyTyped ?

Option 1 : Déclarer le module comme any

Créez un fichier src/types/external.d.ts :

typescript

// Déclare que ce module existe mais sans types précis
declare module 'obscure-library' {
  const content: any;
  export default content;
}

Vous perdez les bénéfices du typage pour cette bibliothèque, mais au moins votre projet compile.

Option 2 : Créer vos propres types

Si vous utilisez intensivement cette bibliothèque, investissez du temps pour créer des types partiels :

typescript

declare module 'obscure-library' {
  export interface Config {
    apiKey: string;
    timeout?: number;
  }
  
  export function initialize(config: Config): void;
  export function fetchData(id: string): Promise<any>;
}

Vous ne typez que ce que vous utilisez réellement. C’est souvent suffisant.

Option 3 : Contribuer à DefinitelyTyped

Si la bibliothèque est populaire, créez une PR sur DefinitelyTyped pour partager vos types avec la communauté. C’est valorisant pour votre profil GitHub et utile pour des milliers de développeurs.


Patterns TypeScript essentiels

Maintenant que vous migrez, adoptons les patterns TypeScript qui feront briller votre code.

Interfaces vs Types

TypeScript offre deux syntaxes pour définir des types : interface et type. Quelle différence ?

Interfaces :

typescript

interface User {
  id: string;
  name: string;
}

// Les interfaces peuvent s'étendre
interface Admin extends User {
  permissions: string[];
}

// Les interfaces peuvent être fusionnées (declaration merging)
interface User {
  email: string; // S'ajoute à la définition précédente
}

Types :

typescript

type User = {
  id: string;
  name: string;
};

// Les types supportent les unions et intersections
type Role = "user" | "admin" | "guest";

type Admin = User & {
  permissions: string[];
};

Règle pratique : utilisez interface pour les objets et les classes. Utilisez type pour les unions, les tuples, et les types utilitaires complexes.

Utility Types intégrés

TypeScript fournit des types utilitaires puissants :

Partial<T> : rend toutes les propriétés optionnelles

typescript

interface User {
  id: string;
  name: string;
  email: string;
}

// Utile pour les mises à jour partielles
function updateUser(id: string, updates: Partial<User>) {
  // updates peut contenir n'importe quelle combinaison de propriétés User
}

updateUser("123", { name: "John" }); // OK
updateUser("123", { email: "john@example.com" }); // OK

Pick<T, K> : sélectionne certaines propriétés

typescript

type UserPreview = Pick<User, 'id' | 'name'>;
// Équivalent à : { id: string; name: string; }

Omit<T, K> : exclut certaines propriétés

typescript

type UserWithoutEmail = Omit<User, 'email'>;
// Équivalent à : { id: string; name: string; }

Record<K, T> : crée un objet avec des clés typées

typescript

type UserRoles = Record<string, "admin" | "user">;

const roles: UserRoles = {
  "user1": "admin",
  "user2": "user"
};

Génériques (Generics)

Les génériques permettent de créer des fonctions et composants réutilisables avec différents types.

typescript

// Sans générique : fonction limitée à un type
function getFirstItemString(array: string[]): string {
  return array[0];
}

// Avec générique : fonction qui fonctionne avec n'importe quel type
function getFirstItem<T>(array: T[]): T {
  return array[0];
}

const firstNumber = getFirstItem([1, 2, 3]); // type: number
const firstString = getFirstItem(["a", "b", "c"]); // type: string

Les génériques brillent vraiment avec les APIs :

typescript

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  return response.json();
}

// TypeScript infère automatiquement le type de data
const userResponse = await fetchApi<User>("/api/user/123");
console.log(userResponse.data.name); // TypeScript sait que data est un User

Type Guards

Les type guards permettent d’affiner les types dans des conditions :

typescript

interface Dog {
  bark(): void;
}

interface Cat {
  meow(): void;
}

type Pet = Dog | Cat;

// Type guard personnalisé
function isDog(pet: Pet): pet is Dog {
  return (pet as Dog).bark !== undefined;
}

function makeSound(pet: Pet) {
  if (isDog(pet)) {
    pet.bark(); // TypeScript sait que pet est un Dog ici
  } else {
    pet.meow(); // TypeScript sait que pet est un Cat ici
  }
}

Enums vs Union Types

Pour les constantes, vous avez le choix entre enum et union types :

typescript

// Enum (génère du JavaScript)
enum Status {
  Pending = "pending",
  Active = "active",
  Archived = "archived"
}

// Union type (pur TypeScript, pas de JS généré)
type Status = "pending" | "active" | "archived";

Recommandation : préférez les union types. Ils sont plus simples, n’ajoutent pas de code JavaScript, et fonctionnent mieux avec les types utilitaires.


Pièges courants et comment les éviter

Tous les développeurs tombent dans ces pièges lors de leur migration. Anticipez-les.

Piège #1 : Ignorer strictNullChecks

Sans strictNullChecks, TypeScript ne distingue pas string de string | null | undefined. Cela cache des bugs potentiels.

typescript

// Sans strictNullChecks
function getUserName(user: User): string {
  return user.name.toUpperCase(); // Peut crasher si user.name est null
}

Solution : activez strictNullChecks et gérez explicitement null/undefined :

typescript

// Avec strictNullChecks
function getUserName(user: User): string {
  return user.name?.toUpperCase() ?? "Anonymous";
}

Piège #2 : Type assertions dangereuses

Les assertions de type (as) forcent TypeScript à accepter votre version, même si elle est fausse.

typescript

// ❌ Dangereux : si data n'est pas vraiment un User, boom en production
const user = data as User;
console.log(user.name.toUpperCase());

Solution : utilisez les type guards et validez vraiment les données :

typescript

// ✅ Sûr : validation avant utilisation
function isUser(data: unknown): data is User {
  return (
    typeof data === "object" &&
    data !== null &&
    "name" in data &&
    typeof (data as User).name === "string"
  );
}

if (isUser(data)) {
  console.log(data.name.toUpperCase()); // Sûr maintenant
}

Piège #4 : Oublier les types de retour explicites

TypeScript infère les types de retour, mais les déclarer explicitement améliore la maintenabilité :

typescript

// ❌ Type de retour inféré, pas clair
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// ✅ Type de retour explicite, documentation claire
function calculateTotal(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

Les types de retour explicites empêchent aussi les régressions : si vous modifiez l’implémentation et changez accidentellement le type retourné, TypeScript vous alertera.

Piège #5 : Types trop complexes

Les développeurs débutants en TypeScript ont tendance à créer des types sur-ingéniérés :

typescript

// ❌ Trop complexe pour ce que ça fait
type ApiResponse<T, E = Error> = {
  data?: T extends Array<infer U> ? U[] : T;
  error?: E;
  metadata: {
    timestamp: number;
    version: string;
  };
} & (
  | { status: 'success'; data: T }
  | { status: 'error'; error: E }
);

Solution : gardez vos types simples et lisibles. Si un type devient trop complexe, décomposez-le :

typescript

// ✅ Simple et clair
interface SuccessResponse<T> {
  status: 'success';
  data: T;
  metadata: Metadata;
}

interface ErrorResponse {
  status: 'error';
  error: Error;
  metadata: Metadata;
}

type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;

Piège #6 : Ne pas typer les événements

Quand vous gérez des événements DOM, typez-les correctement pour accéder aux propriétés spécifiques :

typescript

// ❌ Type générique, perd les propriétés spécifiques
function handleSubmit(e: Event) {
  e.preventDefault();
  const form = e.target; // Type: EventTarget | null
  // form.elements n'existe pas sur EventTarget
}

// ✅ Type spécifique pour accéder aux propriétés du formulaire
function handleSubmit(e: FormEvent<HTMLFormElement>) {
  e.preventDefault();
  const form = e.currentTarget; // Type: HTMLFormElement
  const data = new FormData(form); // Maintenant ça marche !
}

Migration d’un projet React

React et TypeScript forment un couple parfait. Voici comment migrer votre application React existante.

Installation des types React

Si ce n’est pas déjà fait, installez les types pour React :

bash

npm install --save-dev @types/react @types/react-dom

Pour React Router, si vous l’utilisez :

bash

npm install --save-dev @types/react-router-dom

Convertir un composant fonctionnel

Avant (JavaScript) :

jsx

// Button.jsx
import React from 'react';

export function Button({ children, onClick, disabled, variant = 'primary' }) {
  return (
    <button 
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

Après (TypeScript) :

tsx

// Button.tsx
import React from 'react';

interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'danger';
}

export function Button({ 
  children, 
  onClick, 
  disabled = false, 
  variant = 'primary' 
}: ButtonProps) {
  return (
    <button 
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

Améliorations apportées :

  • Interface ButtonProps documente clairement les props acceptées
  • variant utilise un union type pour limiter aux valeurs valides
  • React.ReactNode type correctement children (peut être string, JSX, array, etc.)
  • Les props optionnelles sont marquées avec ?

Typer les hooks useState et useEffect

useState avec typage explicite :

tsx

import { useState } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
}

function UserProfile() {
  // Type inféré automatiquement
  const [count, setCount] = useState(0); // type: number
  
  // Type explicite nécessaire quand initialValue est null
  const [user, setUser] = useState<User | null>(null);
  
  // Type pour tableau
  const [users, setUsers] = useState<User[]>([]);
  
  return (
    <div>
      {user && <p>{user.name}</p>}
    </div>
  );
}

useEffect typé :

tsx

import { useEffect } from 'react';

function DataFetcher() {
  useEffect(() => {
    // TypeScript vérifie que vous retournez bien une cleanup function ou void
    const controller = new AbortController();
    
    fetchData(controller.signal)
      .then(data => console.log(data))
      .catch(err => console.error(err));
    
    // Cleanup function correctement typée
    return () => {
      controller.abort();
    };
  }, []); // TypeScript vérifie les dépendances
  
  return <div>Loading...</div>;
}

Typer les événements React

React a ses propres types d’événements qui étendent les événements DOM natifs :

tsx

import { ChangeEvent, FormEvent, MouseEvent } from 'react';

function Form() {
  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value); // TypeScript connaît target.value
  };
  
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
  };
  
  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
    console.log(e.clientX, e.clientY);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleInputChange} />
      <button onClick={handleClick}>Submit</button>
    </form>
  );
}

Typer useContext

Créez un contexte typé pour éviter les problèmes de null :

tsx

import { createContext, useContext, useState, ReactNode } from 'react';

interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

// Créer le contexte avec undefined par défaut
const AuthContext = createContext<AuthContextType | undefined>(undefined);

// Hook personnalisé qui force la présence du contexte
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// Provider typé
export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  
  const login = async (email: string, password: string) => {
    const user = await apiLogin(email, password);
    setUser(user);
  };
  
  const logout = () => {
    setUser(null);
  };
  
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

Composants génériques

Pour créer des composants vraiment réutilisables, utilisez les génériques :

tsx

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>
          {renderItem(item)}
        </li>
      ))}
    </ul>
  );
}

// Utilisation avec typage automatique
<List
  items={users}
  renderItem={user => <span>{user.name}</span>}
  keyExtractor={user => user.id}
/>

Migration de Redux avec TypeScript

Si vous utilisez Redux, TypeScript améliore drastiquement l’expérience :

typescript

// store/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface UserState {
  currentUser: User | null;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  currentUser: null,
  loading: false,
  error: null
};

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    setUser: (state, action: PayloadAction<User>) => {
      state.currentUser = action.payload;
    },
    setLoading: (state, action: PayloadAction<boolean>) => {
      state.loading = action.payload;
    },
    setError: (state, action: PayloadAction<string>) => {
      state.error = action.payload;
    }
  }
});

export const { setUser, setLoading, setError } = userSlice.actions;
export default userSlice.reducer;

// Types pour useSelector
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Avec ces types, vos hooks Redux sont complètement typés :

tsx

import { useSelector, useDispatch } from 'react-redux';
import type { RootState, AppDispatch } from './store';

function UserProfile() {
  // TypeScript connaît la structure du state
  const user = useSelector((state: RootState) => state.user.currentUser);
  const dispatch = useDispatch<AppDispatch>();
  
  return <div>{user?.name}</div>;
}

Pour approfondir React et TypeScript, consultez notre guide sur les techniques avancées React.


Migration d’un projet Node.js/Express

Côté backend, TypeScript apporte les mêmes bénéfices : moins de bugs, meilleure maintenabilité.

Configuration spécifique Node.js

Ajustez votre tsconfig.json pour Node :

json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "lib": ["ES2022"],
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Installation des types pour Node et Express

bash

npm install --save-dev @types/node @types/express

Typer les routes Express

Avant (JavaScript) :

javascript

// routes/users.js
const express = require('express');
const router = express.Router();

router.get('/:id', async (req, res) => {
  try {
    const user = await getUserById(req.params.id);
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

router.post('/', async (req, res) => {
  try {
    const newUser = await createUser(req.body);
    res.status(201).json(newUser);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

module.exports = router;

Après (TypeScript) :

typescript

// routes/users.ts
import { Router, Request, Response } from 'express';
import { getUserById, createUser } from '../services/userService';
import { User, CreateUserDto } from '../types/user';

const router = Router();

// Typer les paramètres de route
interface UserParams {
  id: string;
}

router.get('/:id', async (req: Request<UserParams>, res: Response) => {
  try {
    const user = await getUserById(req.params.id);
    res.json(user);
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error';
    res.status(500).json({ error: message });
  }
});

// Typer le body de la requête
router.post('/', async (req: Request<{}, {}, CreateUserDto>, res: Response) => {
  try {
    const newUser = await createUser(req.body);
    res.status(201).json(newUser);
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error';
    res.status(400).json({ error: message });
  }
});

export default router;

Typer les middlewares

Les middlewares Express ont une signature spécifique :

typescript

import { Request, Response, NextFunction } from 'express';

// Middleware simple
function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`${req.method} ${req.path}`);
  next();
}

// Middleware avec gestion d'erreur
function errorHandler(
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong!' });
}

// Middleware d'authentification avec extension de Request
declare global {
  namespace Express {
    interface Request {
      user?: User;
    }
  }
}

function authenticate(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    res.status(401).json({ error: 'No token provided' });
    return;
  }
  
  try {
    const decoded = verifyToken(token);
    req.user = decoded; // TypeScript connaît maintenant req.user
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

Typer les services et repositories

Créez des interfaces claires pour vos services :

typescript

// services/userService.ts
import { User, CreateUserDto, UpdateUserDto } from '../types/user';
import { UserRepository } from '../repositories/userRepository';

export class UserService {
  constructor(private userRepository: UserRepository) {}
  
  async getById(id: string): Promise<User> {
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new Error('User not found');
    }
    return user;
  }
  
  async create(data: CreateUserDto): Promise<User> {
    // Validation
    if (!data.email || !data.password) {
      throw new Error('Email and password are required');
    }
    
    return this.userRepository.create(data);
  }
  
  async update(id: string, data: UpdateUserDto): Promise<User> {
    const user = await this.getById(id);
    return this.userRepository.update(id, data);
  }
  
  async delete(id: string): Promise<void> {
    await this.getById(id); // Vérifie que l'utilisateur existe
    await this.userRepository.delete(id);
  }
}

Typer Mongoose (MongoDB)

Si vous utilisez MongoDB avec Mongoose :

typescript

import { Schema, model, Document } from 'mongoose';

// Interface pour le document TypeScript
export interface IUser {
  email: string;
  firstName: string;
  lastName: string;
  password: string;
  createdAt: Date;
}

// Interface pour le document Mongoose (inclut les méthodes)
export interface IUserDocument extends IUser, Document {
  fullName(): string;
}

// Schéma Mongoose
const userSchema = new Schema<IUserDocument>({
  email: { type: String, required: true, unique: true },
  firstName: { type: String, required: true },
  lastName: { type: String, required: true },
  password: { type: String, required: true },
  createdAt: { type: Date, default: Date.now }
});

// Méthode d'instance
userSchema.methods.fullName = function(): string {
  return `${this.firstName} ${this.lastName}`;
};

// Export du modèle typé
export const User = model<IUserDocument>('User', userSchema);

Utilisation :

typescript

const user = await User.findById(userId);
if (user) {
  console.log(user.fullName()); // TypeScript connaît cette méthode
}

Validation avec Zod

Pour valider les données entrantes avec des types TypeScript inférés automatiquement, utilisez Zod :

typescript

import { z } from 'zod';

// Définir le schéma de validation
const createUserSchema = z.object({
  email: z.string().email(),
  firstName: z.string().min(2),
  lastName: z.string().min(2),
  password: z.string().min(8),
  age: z.number().int().positive().optional()
});

// TypeScript infère automatiquement le type depuis le schéma !
type CreateUserDto = z.infer<typeof createUserSchema>;

// Utilisation dans une route
router.post('/', async (req: Request, res: Response) => {
  try {
    // Validation + parsing en une seule ligne
    const userData = createUserSchema.parse(req.body);
    
    // userData est maintenant typé comme CreateUserDto
    const user = await createUser(userData);
    res.status(201).json(user);
  } catch (error) {
    if (error instanceof z.ZodError) {
      res.status(400).json({ errors: error.errors });
    } else {
      res.status(500).json({ error: 'Server error' });
    }
  }
});

Outils et ressources pour accélérer la migration

Ne réinventez pas la roue. Utilisez ces outils pour accélérer votre migration.

ts-migrate : outil automatisé

Airbnb a open-sourcé ts-migrate, un outil qui automatise une grande partie de la migration :

bash

npx ts-migrate migrate <dossier-source>

L’outil :

  • Renomme .js.ts automatiquement
  • Ajoute des types any là où nécessaire
  • Génère les tsconfig.json de base
  • Corrige certains problèmes courants

Attention : l’output nécessite du travail manuel pour remplacer les any par de vrais types, mais ça donne une base solide.

TypeScript Error Translator

Extension VS Code qui traduit les erreurs TypeScript cryptiques en explications claires :

Installation : cherchez « TypeScript Error Translator » dans les extensions VS Code.

Total TypeScript (ressource d’apprentissage)

Site web avec des exercices interactifs pour maîtriser TypeScript : https://www.totaltypescript.com/

Particulièrement utile pour comprendre les génériques, les types conditionnels et les utilitaires avancés.

TypeScript Playground

Testez rapidement du code TypeScript dans le navigateur sans setup : https://www.typescriptlang.org/play

Pratique pour expérimenter avec les types avant de les intégrer dans votre projet.

Assistants IA pour accélérer

Les assistants de code IA comme GitHub Copilot ou Cursor excellent pour générer des types TypeScript. Décrivez votre structure de données en commentaire, et ils génèrent l’interface correspondante.

ESLint + TypeScript

Configurez ESLint avec le plugin TypeScript pour catcher encore plus de problèmes :

bash

npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin

Configuration .eslintrc.js :

javascript

module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended'
  ],
  plugins: ['@typescript-eslint'],
  rules: {
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
  }
};

Prettier pour le formatage

TypeScript fonctionne parfaitement avec Prettier :

bash

npm install --save-dev prettier

Configuration .prettierrc :

json

{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2
}

FAQ

Dois-je migrer tous mes fichiers en une fois ?

Absolument pas ! C’est même déconseillé. Migrez progressivement, fichier par fichier. TypeScript supporte parfaitement la coexistence de fichiers .js et .ts grâce à l’option allowJs. Commencez par les fichiers utilitaires simples, puis étendez progressivement aux fichiers plus complexes.

Combien de temps prend une migration TypeScript ?

Cela dépend de la taille de votre projet. Pour 10 000 lignes de code, comptez 2-4 semaines à temps partiel (20-30% de votre temps). Pour 50 000 lignes, plutôt 2-4 mois. L’important est de ne pas précipiter : une migration bien faite vaut mieux qu’une migration rapide mais bâclée.

Mon projet utilise Webpack/Babel, est-ce compatible avec TypeScript ?

Oui, complètement. Webpack a un loader TypeScript (ts-loader ou babel-loader avec @babel/preset-typescript). Babel peut transpiler TypeScript depuis la version 7. La plupart des build tools modernes (Vite, Parcel, esbuild) supportent TypeScript nativement.

TypeScript ralentit-il la compilation ?

Le checking des types ajoute effectivement du temps de compilation, mais c’est généralement négligeable (quelques secondes). Pour les gros projets, utilisez tsc --incremental qui ne recompile que ce qui a changé. En développement, des outils comme esbuild ou swc compilent TypeScript ultra-rapidement en ignorant le type checking (faites-le en parallèle).

Puis-je utiliser TypeScript sans framework ?

Absolument ! TypeScript fonctionne avec du JavaScript vanilla. Vous pouvez l’utiliser pour du DOM manipulation, des scripts Node.js, ou n’importe quel code JavaScript. Le typage améliore n’importe quel projet, pas seulement les applications React/Angular/Vue.

Comment gérer les fichiers de configuration (webpack.config.js, etc.) ?

Vous pouvez les laisser en JavaScript ou les migrer. Pour webpack, vous pouvez utiliser webpack.config.ts si vous installez ts-node. Mais honnêtement, laisser les configs en JavaScript est parfaitement acceptable. Concentrez vos efforts sur le code applicatif.

Les performances d’exécution sont-elles affectées ?

Non. TypeScript est uniquement un outil de développement. Le code final généré est du JavaScript standard, avec les mêmes performances que si vous aviez écrit du JS directement. Le type checking ne se produit qu’à la compilation, pas à l’exécution.

Dois-je activer strict mode immédiatement ?

Non, commencez avec strict: false. Une fois que 50-70% de votre code est migré, activez progressivement les options strictes une par une : noImplicitAny, puis strictNullChecks, puis finalement strict: true. Cette approche incrémentale évite d’avoir 500 erreurs d’un coup.

Comment convaincre mon équipe/manager de migrer ?

Présentez les bénéfices concrets : réduction des bugs en production (38% selon Airbnb), temps de debugging économisé, meilleure documentation du code, onboarding facilité pour les nouveaux devs. Proposez une migration progressive sur plusieurs sprints sans bloquer les features. Montrez l’amélioration de l’autocomplétion en live. Les résultats parlent d’eux-mêmes.

Que faire avec le code legacy qu’on ne touchera plus jamais ?

Laissez-le en JavaScript. Ne perdez pas de temps à migrer du code qui fonctionne et ne sera jamais modifié. Concentrez vos efforts sur le code actif qui évolue régulièrement. Quand vous devrez le toucher, ce sera le bon moment pour le migrer.

TypeScript fonctionne-t-il bien avec les tests ?

Excellemment. Jest, Vitest, et Mocha supportent tous TypeScript. Vos tests bénéficient aussi du typage : vous attrapez des erreurs dans vos tests avant de les lancer. Configuration exemple pour Jest :

bash

npm install --save-dev ts-jest @types/jest

javascript

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

Puis-je utiliser des nouvelles features JavaScript avant qu’elles soient supportées ?

Oui ! TypeScript supporte souvent les features ECMAScript avant qu’elles n’arrivent dans les navigateurs. Par exemple, les décorateurs, les private fields, etc. Le compilateur transpile ensuite vers du JavaScript compatible avec vos cibles.

Comment maintenir les types à jour avec l’évolution du projet ?

Les types évoluent naturellement avec votre code. Quand vous modifiez une fonction, TypeScript vous force à mettre à jour sa signature. C’est justement l’avantage : impossible d’avoir des types obsolètes. Si vous oubliez quelque chose, le compilateur le détectera immédiatement.1 : Abuser du type any

typescript

// ❌ Mauvais : any désactive complètement TypeScript
function processData(data: any) {
  return data.value.toFixed(2);
}

any annule tous les bénéfices de TypeScript. Utilisez-le uniquement en dernier recours et documentez pourquoi.

Solution : utilisez unknown pour les types vraiment inconnus :

typescript

// ✅ Bon : unknown force à vérifier avant d'utiliser
function processData(data: unknown) {
  if (typeof data === "object" && data !== null && "value" in data) {
    const obj = data as { value: number };
    return obj.value.toFixed(2);
  }
  throw new Error("Invalid data format");
}
Catégories : Javascript

siddhy

Développeur web full stack depuis 2004 dans une agence web du sud de la France et Geek depuis toujours, l'apprentissage et le partage font parti intégrante de ma philosophie au même titre que l'évolution personnelle et la sagesse bouddhiste.

0 commentaire

Laisser un commentaire

Emplacement de l’avatar

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site est protégé par reCAPTCHA et Google Politique de confidentialité et Conditions d'utilisation appliquer.

La période de vérification reCAPTCHA a expiré. Veuillez recharger la page.