Préparation d'entretien 100% Gratuit angularforall.com

Entretien TypeScript Senior — Decorators et Utility Types

Typescript Senior Quiz-Typescript-Senior Typescript-Decorators Mapped-Types Utility-Types Partial Omit Questions-Entretien-Typescript-Senior Qcm-Typescript-Avance Infer Patterns-Ts Architecture-Ts Back-End
Sénior 🔀 Mixte 20 questions ⏱ 15 min
📝

Entretien TypeScript Senior — Decorators et Utility Types

20 questions avancées TypeScript : decorators, mapped types, utility types (Partial, Omit, ReturnType), mot-clé infer, architecture et patterns TypeScript. Idéal pour un entretien Senior.

20 questions ⏱ ~15 min Niveau Sénior

Partager

Banque de révision : les 30 questions corrigées

Voici l'intégralité des 30 questions de « Entretien TypeScript Senior — Decorators et Utility Types », chacune avec sa bonne réponse et son explication. Le quiz interactif ci-dessus en tire une sélection aléatoire à chaque tentative — utilisez cette liste complète pour réviser.

  • TypeScript infère le type le plus spécifique possible ; on peut utiliser as const ou des paramètres génériques avec contraintes pour guider l'inférence (bonne réponse)
  • L'inférence est toujours le type le plus large (string plutôt que "hello")
  • Les génériques ne supportent pas l'inférence
  • On ne peut pas contrôler l'inférence des types littéraux
Explication : TypeScript infère le type le plus spécifique possible. Pour les objets et tableaux, as const force l'inférence vers des types littéraux en lecture seule. Pour les fonctions génériques, on peut contraindre l'inférence avec des paramètres génériques et des contraintes (extends).
Exemple : const config = { name: "test" } as const → type { readonly name: "test" } au lieu de { name: string }.

  • Les interfaces supportent la fusion de déclarations (declaration merging) et sont plus performantes à la compilation ; les types sont plus flexibles mais ne supportent pas la fusion (bonne réponse)
  • Les types supportent la fusion, pas les interfaces
  • Ils sont strictement équivalents
  • Les interfaces sont plus lentes à compiler
Explication : Les interfaces supportent la "declaration merging" : si on déclare deux fois la même interface, elles fusionnent automatiquement. Les types génèrent des erreurs de duplication. De plus, les interfaces sont généralement plus rapides à compiler car elles sont simples à mettre en cache.
Utilisez interface pour les APIs publiques et les objets qui pourraient être étendus. Utilisez type pour les unions, intersections, et types complexes.

  • this est inféré du contexte d'appel ; on peut le typer explicitement comme premier paramètre fictif : function(this: MaClasse, x: number) (bonne réponse)
  • On ne peut pas typer this explicitement
  • this est toujours any
  • Le typage de this se fait avec un décorateur
Explication : TypeScript infère this du contexte où la fonction est appelée. Pour typer explicitement this, on peut utiliser un premier paramètre fictif nommé this avec son type. Ce paramètre disparaît à la compilation.
Exemple : function add(this: { x: number }, y: number) { return this.x + y; }

  • Une fonction qui retourne param is Type pour informer le compilateur du type après vérification (bonne réponse)
  • Une fonction qui retourne un booléen normal
  • Un type spécial pour les conditions
  • Une assertion de type dans une condition
Explication : Un prédicat de type est une fonction de garde (type guard) qui utilise la syntaxe valeur is Type comme type de retour. Elle permet à TypeScript de restreindre le type d'une variable dans la branche conditionnelle.
Exemple : function isString(value: unknown): value is string { return typeof value === "string"; }

  • TypeScript infère les types des paramètres de callback depuis la signature de la fonction parente ; attention aux paramètres optionnels qui peuvent causer des erreurs subtiles (bonne réponse)
  • Les callbacks ne sont jamais inférés
  • Il faut toujours typer manuellement tous les callbacks
  • L'inférence ignore les paramètres optionnels
Explication : TypeScript peut inférer les types des paramètres de callback à partir de la signature attendue. Le piège classique : un paramètre de callback marqué optionnel peut recevoir undefined, ce que le code peut ne pas gérer.
Exemple problématique : arr.forEach((item, index?) => { ... }) - index peut être undefined.

  • La capacité de TypeScript à affiner les types en fonction des vérifications conditionnelles (if, switch, typeof, instanceof) (bonne réponse)
  • Un outil d'analyse statique externe
  • Une fonction de débogage
  • Un type spécial pour les boucles
Explication : L'analyse de flot de contrôle permet à TypeScript de comprendre comment les types évoluent à travers les branches conditionnelles, les boucles, et les retours de fonction. Par exemple, après if (typeof x === "string"), TypeScript sait que x est une string.
Cette analyse fonctionne avec les gardes de type (typeof, instanceof, in, et les prédicats personnalisés).

  • TypeScript utilise une compatibilité structurelle (duck typing) : deux types sont compatibles s'ils ont la même structure, quelle que soit leur dénomination (bonne réponse)
  • TypeScript utilise une compatibilité nominale (basée sur le nom)
  • L'assignabilité est toujours vérifiée à l'exécution
  • Les types sont toujours invariants
Explication : TypeScript a un système de types structurel (ou "duck typing"). Deux types sont compatibles si leur structure correspond, même s'ils ont des noms différents. Par exemple, interface Person { name: string } est compatible avec { name: string }.
Pour créer une compatibilité nominale (basée sur le nom), on peut utiliser des "brands" : type UserId = string & { __brand: "userId" }.

  • La règle qui détermine comment les types génériques se comportent avec l'héritage : les fonctions sont contravariantes sur les paramètres et covariantes sur le retour (bonne réponse)
  • Un concept qui n'existe pas en TypeScript
  • Une mesure de performance
  • Un type de variable
Explication : La variance définit comment les sous-typage se comporte pour les types composés. En TypeScript : les paramètres de fonction sont contravariants (on peut passer un type plus général), le retour est covariant (on peut retourner un type plus spécifique). Les tableaux et les promesses sont covariants par défaut.
Attention : la covariance des tableaux peut causer des erreurs : const nombres: number[] = [1,2]; const valeurs: (number|string)[] = nombres; valeurs.push("hello"); est autorisé mais dangereux.

  • T extends U ? X : Y permet de choisir un type selon une condition ; utilisé pour Filter, ReturnType, ou le typage d'API flexibles (bonne réponse)
  • Une structure if/else pour les valeurs
  • Un type qui dépend de l'exécution
  • Un opérateur de comparaison de types
Explication : Les types conditionnels utilisent la syntaxe T extends U ? Vrai : Faux. Ils sont évalués statiquement. Cas d'usage : type NonNullable<T> = T extends null | undefined ? never : T; ou ReturnType<T>.
Les types conditionnels peuvent être distribués sur les unions : T extends U ? X : Y appliqué à une union évalue la condition pour chaque membre.

  • infer permet d'extraire un type d'un type plus complexe, comme infer R pour capturer le type de retour d'une fonction (bonne réponse)
  • infer est un alias pour any
  • infer permet de créer des types génériques
  • infer est utilisé pour les promesses
Explication : Le mot-clé infer dans un type conditionnel permet de "capturer" un type à inférer. Exemple classique : type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; capture le type de retour de la fonction.
On peut utiliser infer plusieurs fois : T extends [infer First, ...infer Rest] pour extraire le premier élément d'un tuple.

  • keyof T peut retourner string | number | symbol ; & string filtre pour ne garder que les clés de type string (bonne réponse)
  • Il n'y a pas de différence
  • keyof T & string est une erreur de syntaxe
  • keyof T ne retourne que les strings
Explication : En TypeScript, les clés d'un type peuvent être de type string, number, ou symbol (pour les objets avec des clés calculées). keyof T & string est une intersection qui filtre pour ne garder que les clés qui sont assignables à string.
Utilisez keyof T & string quand vous avez besoin de garantir que les clés sont des chaînes (par exemple pour Record<string, ...>).

  • { [P in K]: T[P] } transforme les propriétés ; on peut ajouter readonly et ? avec + ou - (bonne réponse)
  • Les mapped types ne supportent pas les modificateurs
  • On ne peut pas transformer les modificateurs
  • Les modificateurs sont fixes
Explication : Les mapped types itèrent sur les clés pour créer un nouveau type. On peut ajouter/enlever readonly (readonly ou -readonly) et l'optionnalité (? ou -?). Exemple : type Partial<T> = { [P in keyof T]?: T[P] }.
Les modificateurs + sont implicites. Utilisez - pour supprimer : type Required<T> = { [P in keyof T]-?: T[P] }.

  • Permet de créer des types basés sur des templates littéraux comme `${U}` ; utile pour les unions de strings et le typage d'API (bonne réponse)
  • Des chaînes de caractères dynamiques à l'exécution
  • Un nouveau type de littéral
  • Un remplacement des template strings JavaScript
Explication : Les template literal types utilisent la même syntaxe que les template strings JavaScript mais au niveau des types. Exemple : type EventName = `on${Capitalize}` génère toutes les chaînes commençant par "on" suivies d'une lettre majuscule.
Combine avec infer pour parser des chaînes : type GetPath<T> = T extends `/${infer Rest}` ? Rest : never;

  • Une propriété commune (discriminant) permet à TypeScript d'affiner le type dans chaque branche d'un switch ou if (bonne réponse)
  • Les unions ne peuvent pas être discriminées
  • Il faut utiliser un type guard personnalisé
  • Le discriminant doit être un booléen
Explication : Une union discriminée a une propriété commune (souvent type ou kind) dont la valeur littérale permet d'identifier le membre exact. TypeScript utilise cette propriété dans les switch ou if pour affiner le type.
Exemple : type Shape = { kind: "circle"; radius: number } | { kind: "square"; size: number }

  • Déclarer à nouveau une interface ou un module pour ajouter des propriétés ou méthodes (ex: étendre Array) (bonne réponse)
  • Impossible d'étendre des types existants
  • Utiliser le mot-clé extend
  • Créer un nouveau module
Explication : La "module augmentation" permet d'ajouter des membres à des interfaces ou modules existants. On utilise declare module "module" { interface T { ... } }. Exemple typique : ajouter une méthode à l'interface Array ou étendre les types d'Express.
Utile pour typer des bibliothèques JavaScript sans types, ou pour ajouter des méthodes globales à Window.

  • Le this est inféré comme l'instance de la classe ; attention aux callbacks où this peut être perdu (utiliser les fonctions fléchées ou bind) (bonne réponse)
  • Le this est toujours any
  • Les méthodes de classe ne peuvent pas utiliser this
  • Le this est toujours l'objet global
Explication : Dans les méthodes de classe, this est typé comme l'instance de la classe. Mais si on passe la méthode comme callback, this peut être perdu. Solutions : fonctions fléchées (capturent le this lexical) ou bind.
Activez noImplicitThis dans tsconfig.json pour détecter les pertes potentielles de this.

  • object = tout type non-primitif ; Object = tout type avec méthodes Object ; {} = tout sauf null/undefined ; Record&lt;string, unknown&gt; = objet indexable (bonne réponse)
  • Ils sont tous équivalents
  • object est déprécié
  • {} est identique à any
Explication : object : tout type qui n'est pas primitif (string, number, boolean, symbol, null, undefined). Object : tout type avec les méthodes de Object.prototype. {} : tout type sauf null et undefined. Record<string, unknown> : un objet avec des clés string et des valeurs inconnues.
Préférez object ou Record<string, unknown> selon le besoin. Évitez {} et Object qui sont trop permissifs.

  • L'assertion (as T) dit au compilateur "fais-moi confiance" sans vérification ; la déclaration de variable avec annotation (: T) force une vérification (bonne réponse)
  • Ils sont identiques
  • L'assertion est plus sûre que la déclaration
  • La déclaration désactive la vérification
Explication : Une annotation de type (let x: string = valeur) force TypeScript à vérifier que valeur est assignable à string. Une assertion (valeur as string) contourne cette vérification. L'assertion peut masquer des erreurs et doit être utilisée avec précaution.
Utilisez d'abord une annotation de type. Si l'annotation échoue, revoyez votre logique. L'assertion est un dernier recours.

  • Sans l'option, undefined peut être assigné à une propriété optionnelle ; avec, il faut utiliser | undefined explicitement (bonne réponse)
  • L'option rend toutes les propriétés obligatoires
  • L'option supprime les propriétés optionnelles
  • L'option n'a pas d'effet
Explication : Sans exactOptionalPropertyTypes, TypeScript permet d'assigner undefined à une propriété optionnelle (ex: { age?: number } peut recevoir age: undefined). Avec l'option activée, il faut explicitement age?: number | undefined pour autoriser undefined.
Cette option rend le typage plus précis et évite la confusion entre "propriété absente" et "propriété présente avec undefined".

  • Il faut utiliser &lt;T&gt;(x: T) => x ou &lt;T extends {}&gt;(x: T) => x car &lt;T&gt; peut être confondu avec une balise JSX (bonne réponse)
  • Les fonctions fléchées génériques ne sont pas supportées en TSX
  • On utilise function&lt;T&gt;(x: T)
  • Il n'y a pas de différence
Explication : En TSX (TypeScript + JSX), <T> au début d'une expression peut être interprété comme une balise JSX. Pour éviter cela, on utilise <T extends {}> ou on ajoute une virgule <T, >. C'est une particularité de la syntaxe TSX.
La syntaxe const f = <T, >(x: T): T => x; est également valide et plus explicite.

  • Les types sont complètement effacés à la compilation ; les génériques n'existent pas à l'exécution, contrairement à Java/C# (bonne réponse)
  • Les types sont conservés à l'exécution
  • Seuls les génériques sont effacés
  • Les interfaces sont conservées
Explication : TypeScript efface tous les types à la compilation. Contrairement à Java où les génériques sont effacés mais les types de base restent, TypeScript ne conserve aucune information de type à l'exécution. C'est pourquoi instanceof ne fonctionne pas avec les interfaces ou les types génériques.
Pour vérifier un type à l'exécution, utilisez les "type guards" (typeof, instanceof, ou propriétés discriminantes).

  • Les décorateurs (expérimentaux) sont des fonctions spéciales qui modifient les classes, méthodes, propriétés ; ils ne sont pas standardisés et peuvent changer (bonne réponse)
  • Les décorateurs sont complètement stables
  • Les décorateurs fonctionnent comme en Python
  • Les décorateurs sont officiellement supportés en JavaScript
Explication : Les décorateurs TypeScript sont une fonctionnalité expérimentale (flag experimentalDecorators). Ils permettent d'annoter et de modifier les classes et leurs membres. Ils ne font pas encore partie de la spécification ECMAScript finale (proposal stage 3).
Angular utilise intensément les décorateurs. Pour du code générique, préférez d'autres patterns car la syntaxe peut changer.

  • satisfies vérifie qu'une expression correspond à un type sans en changer le type inféré (bonne réponse)
  • Un synonyme de as
  • Une assertion de type inverse
  • Un opérateur pour les promesses
Explication : L'opérateur satisfies permet de vérifier qu'une valeur est compatible avec un type sans étendre le type de la valeur. Exemple : const colors = { red: "#FF0000" } satisfies Record<string, string>; garde l'inférence précise ("red" au lieu de string) tout en vérifiant le type.
Utile pour les objets de configuration où vous voulez garder les types littéraux tout en validant la structure.

  • as const rend un objet ou un tableau en lecture seule et infère les types les plus spécifiques (littéraux) (bonne réponse)
  • as const rend la variable constante
  • as const est identique à const
  • as const convertit en type primitif
Explication : as const appliqué à un objet ou tableau le rend readonly et infère les types littéraux les plus précis. Exemple : const couleurs = ["rouge", "vert"] as const devient readonly ["rouge", "vert"] au lieu de string[].
Très utile pour créer des unions discriminées ou des configurations avec des valeurs exactes.

  • Le structural typing compare les structures ; pour du nominal typing, on utilise des "brands" : type UserId = string & { __brand: "userId" } (bonne réponse)
  • TypeScript supporte le nominal typing nativement
  • On ne peut pas implémenter de nominal typing
  • Le structural typing est basé sur les noms
Explication : Par défaut, TypeScript utilise le structural typing. Pour simuler du nominal typing (où le nom du type compte), on utilise des "brands" (propriétés fantômes). Exemple : type UserId = string & { readonly __brand: unique symbol }.
Les brands sont utiles pour éviter les confusions entre des types structurellement identiques (ex: UserId vs ProductId).

  • Les types récursifs se référencent eux-mêmes ; attention aux limites de profondeur (typiquement 50 niveaux) (bonne réponse)
  • Les types récursifs ne sont pas supportés
  • On peut créer des récursions infinies sans limitation
  • Les types récursifs sont réservés aux listes chaînées
Explication : Les types récursifs s'auto-référencent, comme type JSON = string | number | boolean | null | JSON[] | { [key: string]: JSON }. TypeScript impose une limite de profondeur (configurable) pour éviter les boucles infinies.
Pour les structures profondément récursives, utilisez des interfaces plutôt que des types pour de meilleures performances.

  • asserts value is Type permet de créer des fonctions qui affirment qu'une valeur est d'un certain type, levant une erreur sinon (bonne réponse)
  • Un synonyme de value is Type
  • Une fonction qui retourne un booléen
  • Un opérateur de débogage
Explication : Les "assertion functions" utilisent asserts condition ou asserts value is Type. Si la condition est fausse, la fonction lance une erreur. TypeScript utilise ces assertions pour affiner les types après l'appel.
Exemple : function assertIsString(value: unknown): asserts value is string { if (typeof value !== "string") throw new Error(); }

  • Node resolution imite Node.js (node_modules) ; Classic est hérité. paths dans tsconfig permet des alias de modules (bonne réponse)
  • Les deux modes sont identiques
  • paths ne fonctionne qu'avec Classic
  • Node resolution est déprécié
Explication : Node resolution est le mode par défaut, qui suit les règles de Node.js (cherche dans node_modules). Classic est un ancien mode. L'option paths permet de créer des alias : "paths": { "@app/*": ["./src/app/*"] }.
Pour les chemins personnalisés, assurez-vous que votre bundler (Webpack, Vite) est configuré de la même manière.

  • export type exporte uniquement un type (qui sera effacé à la compilation) et peut aider à casser les cycles (bonne réponse)
  • C'est identique à export normal
  • export type exporte des valeurs
  • Les cycles de types sont impossibles
Explication : export type (et import type) indique que l'export/import concerne uniquement un type, pas une valeur. Cela permet au compilateur de les effacer complètement, ce qui peut casser des cycles d'import circulaires et améliorer les performances.
Utilisez import type quand vous n'avez besoin que du type, pas de la valeur à l'exécution.

  • Utiliser --skipLibCheck, --incremental, projeter séparément les d.ts, utiliser project references, et éviter les types complexes (unions trop grandes) (bonne réponse)
  • Le compilateur est toujours rapide
  • Il n'y a pas de moyens d'optimiser
  • Il faut utiliser Babel à la place
Explication : Les temps de compilation peuvent devenir longs sur de gros projets. Stratégies : --skipLibCheck (ignore les librairies), --incremental (recompile uniquement ce qui a changé), project references (découpe en projets), et éviter les types génériques trop complexes qui explosent combinatoriquement.
Utilisez tsc --generateTrace pour analyser les goulets d'étranglement de compilation.