Questions d'entretien TypeScript
Les questions d'entretien TypeScript testent votre compréhension du typage statique, de l'inférence de type et des fonctionnalités avancées du système de types. Elles sont couramment posées pour les rôles frontend, backend et full-stack pour garantir que vous pouvez écrire du code évolutif et maintenable. Attendez-vous à un mélange d'explications conceptuelles et de problèmes de codage pratiques.
Ce que couvrent les entretiens TypeScript
Types et interfaces
Comprenez la différence entre les alias de type et les interfaces, et quand utiliser chacun. Couvre les types d'objets, les signatures de fonctions et la fusion de déclarations.
Génériques
Sachez comment créer des composants et fonctions réutilisables et type-safe à l'aide de paramètres génériques. Inclut les contraintes et les types par défaut.
Inférence et rétrécissement de type
Montrez comment TypeScript infère les types et les rétrécit en utilisant des conditionnelles, des unions discriminées et des gardes de type.
Types utilitaires et conditionnels
Exploitez les types utilitaires intégrés (par exemple, Partial, Pick) et créez des types conditionnels personnalisés pour transformer les types dynamiquement.
Exemples de questions d'entretien TypeScript
- Expliquez la différence entre `type` et `interface` en TypeScript. Quand utiliseriez-vous l'un plutôt que l'autre ?Ce qu'une bonne réponse couvre
- Les interfaces peuvent être étendues (extends) alors que les types utilisent l'intersection (&).
- Les interfaces peuvent être fusionnées (déclarations multiples) alors que les types non.
- Les types peuvent être des unions, des tuples, ou des primitives, pas les interfaces.
- Utilisez interface pour des objets à étendre ou pour des API publiques; type pour des compositions complexes.
- Les types offrent des fonctionnalités comme les types conditionnels et les mapped types plus directement.
Voir un exemple de réponse
En TypeScript, `type` et `interface` sont souvent interchangeables pour définir la forme d'un objet. Cependant, `interface` supporte le merging de déclarations (déclaration fusion) et peut être étendu via `extends`. `type` est plus flexible : il peut représenter des unions, des intersections, des tuples, des primitives, et utiliser des types conditionnels. En pratique, on utilise `interface` pour des objets qui seront étendus (par exemple, dans des bibliothèques) ou pour des API publiques, car le merging facilite l'extension. On utilise `type` pour des compositions complexes, des unions, ou quand on a besoin de fonctionnalités avancées comme les mapped types. Il faut noter que choisir l'un n'empêche pas d'utiliser l'autre, mais la cohérence dans un projet est importante.
- Implémentez un type générique `DeepReadonly<T>` qui rend toutes les propriétés et propriétés imbriquées en lecture seule.Ce qu'une bonne réponse couvre
- DeepReadonly est un type récursif qui rend toutes les propriétés en lecture seule.
- Il utilise des types conditionnels pour vérifier si une propriété est un objet.
- On combine `keyof` et `in` pour itérer sur les propriétés.
- Attention : il ne gère pas les tableaux ni les fonctions par défaut.
- On peut étendre pour gérer les cas spéciaux si nécessaire.
Voir un exemple de réponse
Pour implémenter `DeepReadonly<T>`, on utilise un type mappé récursif. On vérifie si la propriété est un objet (non primitif) et si oui, on applique récursivement `DeepReadonly` sur sa valeur. Sinon, on la marque `readonly`. Il faut faire attention à exclure les fonctions (car `typeof` fonctionne souvent sur les fonctions) et les tableaux. Voici une implémentation simple :
Solution de référencetypescript type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends Function ? T[P] : T[P] extends object ? DeepReadonly<T[P]> : T[P]; }; // Exemple d'utilisation interface User { name: string; address: { street: string; zip: number; }; } type ReadonlyUser = DeepReadonly<User>; // Toutes les propriétés sont readonly, y compris address.street - Écrivez une fonction générique `filterArray<T>(arr: T[], predicate: (item: T) => item is T): T[]` qui filtre en fonction d'un prédicat de type.Ce qu'une bonne réponse couvre
- Le prédicat de type `item is T` permet au compilateur de réduire le type après filtrage.
- La fonction renvoie un tableau filtré du même type T.
- On utilise simplement `arr.filter(predicate)` car le type est compatible.
- Utile pour les unions discriminantes où le prédicat affine le type.
- Attention : le prédicat doit être une fonction de type garde.
Voir un exemple de réponse
On peut implémenter `filterArray` en déléguant à `Array.filter`. `Array.filter` accepte un prédicat de type `(value: T, index: number, array: T[]) => value is S` où S est un sous-type de T. Dans notre cas, on veut que le prédicat soit `(item: T) => item is T`, donc la signature est exactement celle attendue par `filter` pour un type garde. Ainsi, la fonction retourne `T[]`. C'est un simple wrapper. Utilisation :
Solution de référencetypescript function filterArray<T>(arr: T[], predicate: (item: T) => item is T): T[] { return arr.filter(predicate); } // Exemple : filtrer les nombres d'un tableau mixed const mixed: (string | number)[] = [1, 'hello', 2, 'world']; const numbers = filterArray(mixed, (item): item is number => typeof item === 'number'); // numbers est de type number[] console.log(numbers); // [1, 2] - Qu'est-ce que le type `unknown` et en quoi diffère-t-il de `any` ? Fournissez un exemple de quand utiliser chacun.Ce qu'une bonne réponse couvre
- `unknown` est le type sécurisé : on ne peut rien faire avec sans assertion de type.
- `any` désactive complètement la vérification de type.
- `unknown` force le développeur à vérifier le type avant utilisation.
- Utilisez `unknown` pour des valeurs externes (API, JSON.parse) ; `any` pour des migrations rapides.
- `any` peut cacher des erreurs, `unknown` est préférable pour la sécurité.
Voir un exemple de réponse
Le type `unknown` représente une valeur dont le type est inconnu mais qui doit être vérifié avant utilisation. Contrairement à `any`, qui autorise toutes les opérations sans vérification, `unknown` ne permet aucune opération (affectation à d'autres types, accès aux propriétés, etc.) sans une assertion de type explicite ou une vérification de type. Par exemple, `JSON.parse` retourne `any` par défaut, mais on peut le typer pour retourner `unknown`. On utilise `unknown` quand on n'est pas sûr du type d'une valeur mais qu'on veut s'assurer de vérifier son type plus tard. `any` est à éviter car il désactive la vérification statique ; on l'utilise seulement temporairement ou pour des valeurs vraiment dynamiques.
- Comment typer une fonction asynchrone qui retourne une Promise d'un objet utilisateur ? Montrez comment gérer les états résolu et rejeté.Ce qu'une bonne réponse couvre
- On type la fonction avec `Promise<User>` où User est l'interface.
- Pour gérer l'état résolu, le type de retour est `Promise<User>`.
- Pour l'état rejeté, on peut utiliser `try/catch` ou `.catch()`.
- Le type d'erreur est inconnu (unknown) en général.
- On peut utiliser `async/await` et gérer les erreurs avec un type union.
Voir un exemple de réponse
On définit d'abord une interface `User`. La fonction asynchrone retourne `Promise<User>`. Dans l'implémentation, on utilise `async` et `await`. Pour gérer les erreurs, on encapsule dans un `try/catch`. On peut typer la valeur retournée en cas d'erreur avec un type union (par exemple `User | Error`), mais généralement on laisse l'erreur se propager ou on la gère localement. Exemple :
Solution de référencetypescript interface User { id: number; name: string; } async function fetchUser(id: number): Promise<User> { try { const response = await fetch(`/users/${id}`); if (!response.ok) { throw new Error('Échec de la requête'); } const data: User = await response.json(); return data; } catch (error: unknown) { // Gérer l'erreur, par exemple rethrow avec un type throw new Error(`Erreur lors du chargement de l'utilisateur : ${error}`); } } // Usage fetchUser(1) .then(user => console.log(user.name)) .catch(err => console.error(err.message)); - Concevez une classe d'émetteur d'événements type-safe qui impose les noms d'événements et les types de charge utile.Ce qu'une bonne réponse couvre
- Utiliser un type générique pour les noms d'événements et un mapping pour les payloads.
- La classe stocke les callbacks par type d'événement.
- Les méthodes `on`, `off`, `emit` sont typées avec le payload correspondant.
- On peut utiliser un type Record pour mapper événement → payload.
- Attention : pour un type-safe parfait, il faut utiliser un type conditionnel ou des génériques.
Voir un exemple de réponse
Pour une classe d'émetteur d'événements type-safe, on définit un type qui associe chaque nom d'événement à sa charge utile. On stocke les callbacks dans un objet `Map` ou un `Record` typé. Les méthodes `on` et `emit` utilisent des génériques pour lier le nom et le payload. Exemple simple :
Solution de référencetypescript // Définir un type d'événements avec leurs payloads type EventMap = { userLoggedIn: { userId: number }; userLoggedOut: undefined; error: { message: string }; }; class TypedEventEmitter<T extends Record<string, unknown>> { private listeners: Map<keyof T, Array<(payload: T[keyof T]) => void>> = new Map(); on<K extends keyof T>(event: K, callback: (payload: T[K]) => void): void { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event)!.push(callback as (payload: T[keyof T]) => void); } off<K extends keyof T>(event: K, callback: (payload: T[K]) => void): void { const cbs = this.listeners.get(event); if (cbs) { const idx = cbs.indexOf(callback as (payload: T[keyof T]) => void); if (idx > -1) cbs.splice(idx, 1); } } emit<K extends keyof T>(event: K, payload: T[K]): void { const cbs = this.listeners.get(event); if (cbs) { cbs.forEach(cb => cb(payload)); } } } // Utilisation const emitter = new TypedEventEmitter<EventMap>(); emitter.on('userLoggedIn', (payload) => { console.log(payload.userId); // type-safe }); emitter.emit('userLoggedIn', { userId: 123 }); - Expliquez comment fonctionnent les opérateurs `keyof` et `typeof`. Fournissez des exemples de leur utilisation pour créer des types dynamiques.Ce qu'une bonne réponse couvre
- `keyof T` renvoie une union des clés de T.
- `typeof x` renvoie le type statique de x.
- `keyof` crée des types dynamiques comme `Record<keyof T, ...>`.
- `typeof` est utile pour capturer le type d'un objet littéral ou d'une fonction.
- Combinés, ils permettent de créer des types mappés et conditionnels.
Voir un exemple de réponse
`keyof` est un opérateur de type qui prend un type objet et retourne une union de ses noms de propriétés (clés). Par exemple, `keyof { name: string, age: number }` donne `'name' | 'age'`. `typeof` est un opérateur de type ou de valeur qui, lorsqu'utilisé dans un contexte de type, retourne le type de l'expression. Par exemple, `const x = { a: 1 }; type X = typeof x;` donne `{ a: number }`. Ensemble, `keyof typeof obj` permet d'obtenir les clés d'un objet concret. Exemple : créer un type qui mappe chaque propriété d'un objet à son type : `type PropTypes<T> = { [K in keyof T]: T[K] }`. `typeof` est aussi utilisé pour obtenir le type d'une fonction : `type F = typeof someFunction`.
- Utilisez les types mappés pour créer un type `FlagProperties<T>` qui convertit toutes les propriétés booléennes d'un type d'objet `T` en littéral de chaîne 'true' | 'false'.Ce qu'une bonne réponse couvre
- On utilise un type mappé avec `[K in keyof T]`.
- On vérifie si `T[K]` est `boolean` avec un type conditionnel.
- Si c'est boolean, on la transforme en `'true' | 'false'`.
- Sinon, on laisse le type inchangé.
- Attention : les propriétés optionnelles doivent être gérées.
Voir un exemple de réponse
Pour créer `FlagProperties<T>`, on utilise un type mappé qui itère sur chaque clé de `T`. Pour chaque propriété, on vérifie si son type est `boolean`. Si oui, on la transforme en `'true' | 'false'` (union de deux littéraux). Sinon, on laisse le type original. Voici une implémentation :
Solution de référencetypescript type FlagProperties<T> = { [K in keyof T]: T[K] extends boolean ? 'true' | 'false' : T[K]; }; // Exemple d'utilisation interface Options { debug: boolean; name: string; }; type Flagged = FlagProperties<Options>; // Résultat : { debug: 'true' | 'false'; name: string }
Comment se préparer
- Entraînez-vous en implémentant des types utilitaires à partir de zéro (par exemple, Partial, Pick, ReturnType).
- Comprenez le typage structurel (duck typing) et comment il affecte la compatibilité des types.
- Maîtrisez les motifs avancés comme les unions discriminées et les types marqués.
- Utilisez le terrain de jeu TypeScript et les défis officiels (par exemple, type challenges) pour affiner vos compétences.
- Révisez des bases de code TypeScript réelles (par exemple, des bibliothèques React ou Node.js) pour voir les motifs en action.
Questions fréquemment posées
Dois-je connaître TypeScript pour un poste React ?
Oui, car la plupart des bases de code React modernes utilisent TypeScript pour détecter les erreurs tôt et améliorer l'expérience développeur.
Comment puis-je m'entraîner à TypeScript pour les entretiens ?
Utilisez le terrain de jeu TypeScript, résolvez des défis de type sur GitHub (par exemple, type-challenges) et implémentez de petits projets.
Quelles sont les erreurs courantes dans les entretiens TypeScript ?
Utiliser excessivement `any`, ignorer le mode strict et ne pas comprendre comment fonctionnent les types conditionnels.
TypeScript est-il difficile à apprendre ?
Les bases sont faciles, mais les fonctionnalités avancées comme les types conditionnels et les types de littéraux de modèle nécessitent de la pratique.
Dois-je mémoriser les définitions de type ?
Concentrez-vous sur la compréhension des concepts plutôt que sur la mémorisation. Savoir quand utiliser les génériques ou les types utilitaires est plus important.
Pratiquez les questions TypeScript avec des retours instantanés de l'IA
Téléchargez votre CV, obtenez un entretien simulé personnalisé et voyez exactement ce qu'il faut améliorer — gratuit pour commencer.