Si vous écrivez du JavaScript et que vous n'utilisez pas encore TypeScript, vous vous privez de l'outil le plus efficace pour éviter les bugs en production. TypeScript n'est pas un langage différent — c'est du JavaScript avec un typage statique. Le compilateur vous signale les erreurs avant que le code ne s'exécute, pas après. Dans ce guide, je présente tout ce dont vous avez besoin pour commencer à utiliser TypeScript dans des projets réels.
Pourquoi TypeScript
JavaScript est dynamique — n'importe quelle variable peut être n'importe quoi à n'importe quel moment. C'est flexible, mais dangereux. Vous ne découvrez les erreurs que lorsque le code s'exécute et que l'utilisateur voit l'erreur.
// JavaScript : ceci s'exécute sans erreur
function calculateTotal(price, quantity) {
return price * quantity
}
calculateTotal('10', 2) // Renvoie '102' (string), pas 20
calculateTotal(10) // Renvoie NaN, sans avertissementTypeScript attrape ces erreurs avant l'exécution :
// TypeScript : le compilateur vous avertit
function calculateTotal(price: number, quantity: number): number {
return price * quantity
}
calculateTotal('10', 2) // ✗ Erreur : Argument of type 'string' is not assignable to 'number'
calculateTotal(10) // ✗ Erreur : Expected 2 arguments, but got 1Types de base
Les types primitifs de TypeScript correspondent directement aux types de JavaScript :
// Primitifs
let name: string = 'Vinicius'
let age: number = 24
let isActive: boolean = true
// Arrays
let tags: string[] = ['react', 'next', 'typescript']
let scores: number[] = [95, 87, 92]
// Objets inline
let user: { name: string; email: string } = {
name: 'Vinicius',
email: 'vini@email.com'
}En pratique, vous n'avez presque jamais besoin de typer explicitement les variables — TypeScript infère le type automatiquement :
// Inférence automatique — TS sait que c'est une string
let name = 'Vinicius' // type : string
let age = 24 // type : number
let tags = ['react'] // type : string[]
// Ne typer explicitement que si nécessaire
let data: unknown = await fetchFromAPI()Interfaces et Types
Pour des objets plus complexes, utilisez interface ou type. La différence pratique est minime — utilisez interface pour les objets et type pour les unions et les types composés :
// Interface — pour les objets
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
avatar?: string // champ optionnel
}
// Type — pour les unions et les compositions
type Status = 'pending' | 'confirmed' | 'failed' | 'refunded'
type ApiResponse<T> = {
data: T
success: boolean
error: string | null
}Les interfaces peuvent être étendues, ce qui est utile pour l'héritage de types :
interface BaseEntity {
id: string
createdAt: Date
updatedAt: Date
}
interface User extends BaseEntity {
name: string
email: string
}
interface Order extends BaseEntity {
userId: string
total: number
status: Status
}
// User a maintenant id, createdAt, updatedAt, name, emailFonctions typées
C'est dans le typage des fonctions que TypeScript brille le plus. Le compilateur valide les arguments et le retour :
// Paramètres et retour typés
function createUser(name: string, email: string): User {
return {
id: crypto.randomUUID(),
name,
email,
role: 'user',
createdAt: new Date(),
updatedAt: new Date()
}
}
// Arrow function
const formatPrice = (cents: number): string => {
return `R$ ${(cents / 100).toFixed(2)}`
}
// Fonction asynchrone
async function getUser(id: string): Promise<User | null> {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) return null
return res.json()
}Generics : des types réutilisables
Les generics permettent de créer des fonctions et des types qui fonctionnent avec n'importe quel type, tout en conservant la sécurité. C'est le concept le plus puissant de TypeScript :
// Sans generics — il faudrait une fonction par type
function getFirstString(arr: string[]): string | undefined {
return arr[0]
}
function getFirstNumber(arr: number[]): number | undefined {
return arr[0]
}
// Avec generics — une fonction pour tous les types
function getFirst<T>(arr: T[]): T | undefined {
return arr[0]
}
getFirst(['a', 'b', 'c']) // type : string | undefined
getFirst([1, 2, 3]) // type : number | undefined
getFirst<User>(users) // type : User | undefinedLes generics sont essentiels pour les fonctions d'API, les hooks React et tout code réutilisable :
// Réponse d'API générique
async function fetchAPI<T>(endpoint: string): Promise<ApiResponse<T>> {
const res = await fetch(`/api${endpoint}`)
const data = await res.json()
return data as ApiResponse<T>
}
// Utilisation — TS connaît le type de retour
const { data: user } = await fetchAPI<User>('/users/123')
// user est de type User
const { data: orders } = await fetchAPI<Order[]>('/orders')
// orders est de type Order[]Utility Types : transformer les types
TypeScript est livré avec des utility types qui transforment les types existants. Voici ceux que j'utilise au quotidien :
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
// Partial — tous les champs deviennent optionnels
type UpdateUser = Partial<User>
// { id?: string; name?: string; email?: string; role?: ... }
// Pick — sélectionne des champs spécifiques
type UserPreview = Pick<User, 'id' | 'name'>
// { id: string; name: string }
// Omit — retire des champs spécifiques
type CreateUser = Omit<User, 'id'>
// { name: string; email: string; role: 'admin' | 'user' }
// Record — crée un objet typé
type UserMap = Record<string, User>
// { [key: string]: User }L'usage le plus courant se trouve dans les formulaires et les API — là où vous avez besoin de variations du même type :
// API : créer un utilisateur (sans id, la base de données le génère)
async function createUser(data: Omit<User, 'id'>): Promise<User> {
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
})
return res.json()
}
// API : mettre à jour partiellement (tous les champs optionnels)
async function updateUser(id: string, data: Partial<User>): Promise<User> {
const res = await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(data)
})
return res.json()
}Enums vs Union Types
Les enums existent en TypeScript mais, en pratique, les union types sont plus simples et ne génèrent pas de code JavaScript supplémentaire :
// ❌ Enum — génère du code JS supplémentaire, plus complexe
enum PaymentStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
FAILED = 'failed',
}
// ✅ Union type — plus simple, zéro overhead
type PaymentStatus = 'pending' | 'confirmed' | 'failed'
// Les deux fonctionnent pareil pour la validation :
function processPayment(status: PaymentStatus) {
switch (status) {
case 'pending':
return 'En attente...'
case 'confirmed':
return 'Confirmé !'
case 'failed':
return 'Échec.'
}
}Type guards : affiner les types
Quand une valeur peut être de plusieurs types, les type guards aident TypeScript à comprendre de quel type il s'agit à chaque moment :
type Result = { success: true; data: User } | { success: false; error: string }
function handleResult(result: Result) {
if (result.success) {
// TS sait que result.data existe ici
console.log(result.data.name)
} else {
// TS sait que result.error existe ici
console.error(result.error)
}
}
// Type guard personnalisé
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'email' in value
)
}
const data: unknown = await fetchSomething()
if (isUser(data)) {
// TS sait que data est un User ici
console.log(data.email)
}Configuration : tsconfig.json
Le tsconfig.json définit le comportement de TypeScript dans le projet. Pour des projets modernes avec Next.js ou Node.js, voici la base que je recommande :
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}Le plus important est "strict": true — il active toutes les vérifications de type. Les projets sans strict mode perdent la plus grande partie de la valeur de TypeScript.
Les patterns que j'utilise en production
Après avoir utilisé TypeScript dans tous les projets, voici les patterns que j'applique le plus au quotidien :
- **Ne jamais utiliser
any** — utilisezunknownquand vous ne connaissez pas le type et faites un type guard pour valider - L'inférence d'abord — laissez TS inférer le type chaque fois que c'est possible, ne typez explicitement que si nécessaire
- Des interfaces pour les props de composants — toute prop de composant React doit avoir une interface
- Des utility types pour les API —
Omit<User, 'id'>pour la création,Partial<User>pour l'update - Des union types au lieu des enums — plus simples, sans overhead de runtime
- **
as const** — pour les arrays et les objets littéraux qui ne doivent pas changer
// as const — transforme des valeurs en types littéraux
const ROLES = ['admin', 'user', 'moderator'] as const
type Role = typeof ROLES[number] // 'admin' | 'user' | 'moderator'
// Props de composant React
interface ButtonProps {
label: string
variant?: 'primary' | 'secondary' | 'ghost'
onClick: () => void
disabled?: boolean
}
function Button({ label, variant = 'primary', onClick, disabled }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled} className={variant}>
{label}
</button>
)
}Conclusion
TypeScript ne consiste pas à écrire plus de code — il s'agit d'écrire du code que le compilateur peut valider. Les types documentent l'intention, les interfaces définissent des contrats entre modules, et les generics permettent la réutilisation sans perdre en sécurité. Si vous venez de JavaScript, la courbe d'apprentissage est douce : commencez par typer les fonctions et les props, puis avancez vers les generics et les utility types. Le gain en productivité et en confiance dans le code apparaît vite.
