$articleFAQ = [ [ 'question' => 'Qu\'est-ce qu\'un barrel export en TypeScript et quand l\'utiliser ?', 'answer' => 'Un barrel export est un fichier index.ts qui ré-exporte les symboles d\'un dossier pour offrir un point d\'entrée unique : export { UserService } from \'./user.service\'; export type { User } from \'./user.model\'. Les consommateurs écrivent import { UserService, User } from \'@features/users\' au lieu de deux imports séparés. À utiliser pour les surfaces API stables (libs publiques, modules métier). À éviter sur les feuilles de l\'arbre de dépendances : sur des dossiers contenant beaucoup d\'exports inutilisés en parallèle, le barrel charge tout et pénalise le tree-shaking si le bundler ne fait pas un sideEffects: false propre.', ], [ 'question' => 'Pourquoi utiliser le path aliasing TypeScript ?', 'answer' => 'Trois raisons mesurables. (1) Lisibilité : import from \'@features/auth/services\' contre import from \'../../../features/auth/services\' — l\'œil suit la sémantique métier, pas la position dans l\'arbre de fichiers. (2) Refactor sans casse : déplacer un dossier ne casse PAS les imports qui passent par l\'alias (ils pointent vers la racine du module, pas une position relative). (3) Cohérence monorepo : un seul @my-app/shared partagé entre Angular et NestJS au lieu de chemins relatifs distincts par projet. Configuration dans tsconfig.json paths + résolution équivalente dans le bundler (Vite, esbuild, Angular CLI).', ], [ 'question' => 'Barrel exports vs imports directs : quels tradeoffs réels en 2026 ?', 'answer' => 'Barrel : API stable, refactor interne caché, encapsulation. Inconvénients : (1) Tree-shaking dégradé si la lib n\'a pas "sideEffects": false dans package.json, (2) augmentation du temps de compilation TS sur les très gros barrels (200+ exports), (3) circular dependency cachées par le barrel intermédiaire. Imports directs : tree-shake parfait, compilation TS plus rapide, dépendances explicites. Recommandation 2026 : barrels uniquement sur les libs partagées (libs/shared-types) avec exports nommés explicites (jamais export *), imports directs au sein d\'un même module. Vérifier avec rollup-plugin-analyzer le bundle final.', ], [ 'question' => 'Comment éviter les dépendances circulaires avec les barrels ?', 'answer' => 'Trois règles strictes. (1) Un barrel ne doit JAMAIS importer un autre barrel du même module — toujours descendre vers les fichiers concrets. (2) Si deux modules s\'importent mutuellement, c\'est qu\'ils devraient n\'en faire qu\'un (ou être tirés dans un troisième module commun). (3) Activer "noCircularDependencies" via le plugin ESLint import/no-cycle ou run npx madge --circular src/ dans la CI — détection automatique. Symptôme classique : "Cannot access \'X\' before initialization" en runtime alors que TypeScript compile sans erreur. Cause typique : un barrel A exporte B qui importe A. Refactor : extraire B dans un module séparé sans dépendance vers A.', ], [ 'question' => 'Quelle différence entre import type et import en TypeScript ?', 'answer' => 'import type { User } : import strictement type-only, supprimé à la compilation, aucun code JavaScript émis. Idéal pour les types et interfaces. import { User } : import classique — TypeScript suppose que User pourrait être runtime (classe, valeur, enum), conserve l\'import dans le JS émis. Avec verbatimModuleSyntax: true (TS 5.0+), TypeScript force l\'usage explicite : si User est un type, vous DEVEZ écrire import type. Bénéfice : élimine les imports parasites qui apparaissaient dans le bundle final (notamment sur les types Zod, interfaces de DTO). Migration : code mod ts-fix-imports ou eslint-plugin-import avec règle consistent-type-imports.', ], [ 'question' => 'Comment configurer tsconfig.json pour le path aliasing ?', 'answer' => 'Dans tsconfig.json : "baseUrl": "./", "paths": { "@core/*": ["src/app/core/*"], "@features/*": ["src/app/features/*"], "@shared/*": ["src/app/shared/*"] }. Côté bundler : Angular CLI et Nx lisent automatiquement tsconfig.json. Vite : ajouter vite-tsconfig-paths plugin (npm i -D vite-tsconfig-paths) puis plugin tsconfigPaths() dans vite.config.ts. Webpack : tsconfig-paths-webpack-plugin. Pour Node.js direct (NestJS), ajouter "moduleResolver": "tsconfig-paths/register" dans node --import. ATTENTION : les paths du tsconfig sont compile-time uniquement — sans configuration bundler, le runtime échoue avec "Cannot find module".', ], [ 'question' => 'moduleResolution : node16, nodenext, bundler — lequel choisir en 2026 ?', 'answer' => 'bundler (TS 5.0+) : recommandé pour les apps frontend (Angular, React, Vue) bundle-ées via Vite, esbuild, Angular CLI, webpack. Plus permissif sur les extensions (.js implicite, paths libres), correspond exactement à ce que font les bundlers modernes. node16/nodenext : à utiliser pour les libs publiées sur npm + apps Node.js pures (NestJS, CLI). Force les extensions .js explicites dans les imports, respecte le package.json exports, supporte ESM + CJS dual. classic : obsolète, à ne plus utiliser (résolution legacy d\'avant 2018). Règle simple : si vous bundlez avec Vite/Angular CLI → bundler. Si vous publiez sur npm ou produisez un binaire Node → nodenext.', ], [ 'question' => 'Comment écrire un fichier .d.ts pour typer une lib JavaScript sans types ?', 'answer' => 'Trois patterns selon le scénario. (1) Module externe : créer typings/my-lib.d.ts avec declare module \'my-lib\' { export function foo(x: number): string; export default foo; }. (2) Augmentation d\'une lib existante (ajouter un champ à Express Request) : créer typings/express.d.ts avec declare module \'express\' { interface Request { user?: User } }. (3) Variable globale (lib injectée via CDN) : créer typings/globals.d.ts avec declare global { interface Window { __MY_APP__: AppInstance } }. Inclure le dossier typings/ dans tsconfig.json via "include": ["src/**/*", "typings/**/*"]. Référence : DefinitelyTyped (npm @types/*) couvre déjà 8000+ libs populaires.', ], [ 'question' => 'Comment partager des types entre frontend Angular et backend NestJS dans un monorepo ?', 'answer' => 'Pattern Nx/Turborepo : créer libs/shared-types/ avec src/index.ts comme barrel. Configurer dans tsconfig.base.json : "paths": { "@my-app/shared-types": ["libs/shared-types/src/index.ts"] }. La lib expose uniquement des types et schémas Zod (zéro dépendance Angular/Node) : type User, type CreateUserDto, const UserSchema = z.object({...}). Frontend et backend importent identiquement via @my-app/shared-types. Vérification : ajouter un test e2e qui parse la même valeur des deux côtés. Bénéfice mesuré : refactor d\'un champ DTO casse instantanément les 2 apps à la compilation — aucune incompatibilité runtime possible.', ], [ 'question' => 'Quelles règles ESLint activer pour la qualité des imports TypeScript ?', 'answer' => 'Cinq règles essentielles via eslint-plugin-import. (1) import/order : ordre standardisé (built-in, external, internal, parent, sibling) — supprime les diff parasites. (2) import/no-cycle : détecte les dépendances circulaires (max-depth 4). (3) import/no-relative-parent-imports : interdit les ../ — force l\'usage des alias. (4) @typescript-eslint/consistent-type-imports : impose import type pour les types-only. (5) import/no-default-export : déconseillé sauf cas spécifique (composants Lazy Angular/Vue). Setup typique pour un projet Angular 17+ : voir le preset @nx/eslint-plugin/typescript qui couvre 90 % des bonnes pratiques. Activer en CI avec ESLint en mode strict (--max-warnings 0).', ], ]; TypeScript : modules, barrel exports et path aliasing | AngularForAll
Front-end angularforall.com

- TypeScript : modules, barrel exports et path aliasing

Typescript Modules Typescript Barrel Exports Path Aliasing Tsconfig Paths Organisation Code Typescript
TypeScript : modules, barrel exports et path aliasing

Maîtrisez l'organisation d'un projet Angular avec les ES Modules, barrel exports (index.ts), path aliasing tsconfig et détection des dépendances.

ES Modules vs CommonJS en TypeScript

TypeScript peut compiler vers deux systemes de modules radicalement differents : ES Modules (ESM) et CommonJS (CJS). Comprendre leurs differences est fondamental pour eviter des bugs subtils au moment du bundling ou de l'execution Node.js.

CommonJS : le systeme historique de Node.js

CommonJS utilise require() et module.exports. Les imports sont resolus dynamiquement a l'execution, ce qui rend le tree-shaking difficile pour les bundlers.

// CommonJS — syntaxe historique Node.js
// Les imports sont resolus au moment de l'execution (dynamique)
const express = require('express');
const { readFileSync } = require('fs');

// Export d'un objet ou d'une valeur
module.exports = {
  startServer: function() { /* ... */ }
};

ES Modules : le standard moderne

Les ES Modules utilisent import / export statiques. Les imports sont analyses a la compilation, ce qui permet le tree-shaking et l'analyse statique des dependances.

// ES Modules — syntaxe standard (TypeScript et navigateurs modernes)
// Les imports sont resolus statiquement (analyses a la compilation)
import { Injectable } from '@angular/core';
import type { User } from './models/user.model';

// Named export : on exporte une fonction nommee
export function formatDate(date: Date): string {
  return date.toLocaleDateString('fr-FR');
}

// Default export : un seul par module (a eviter en Angular)
export default class AppService {
  // ...
}

Que choisir en TypeScript / Angular ?

Angular utilise exclusivement les ES Modules. Le fichier tsconfig.json doit specifier le module cible via la propriete "module".

// tsconfig.json — configuration du systeme de modules
{
  "compilerOptions": {
    // "ESNext" preserve les import/export pour que le bundler les traite
    // Ne jamais utiliser "CommonJS" dans un projet Angular
    "module": "ESNext",

    // "bundler" est le mode de resolution recommande avec Vite/esbuild
    "moduleResolution": "bundler",

    // Cible d'execution JavaScript (ES2022 recommande pour Angular 17+)
    "target": "ES2022"
  }
}
Critere CommonJS ES Modules
Syntaxe require() / module.exports import / export
Resolution Dynamique (runtime) Statique (compile time)
Tree-shaking Difficile ou impossible Natif et efficace
Top-level await Non supporte Supporte
Usage Angular Non recommande Standard obligatoire
A retenir : dans un projet Angular, toujours utiliser les ES Modules avec "module": "ESNext". CommonJS est reserve aux scripts Node.js legacy ou aux outils de build.

import type vs import : verbatimModuleSyntax

TypeScript 3.8 a introduit import type, et TypeScript 5.0 a renforce son usage avec l'option verbatimModuleSyntax. Cette distinction a un impact direct sur la taille du bundle et la compatibilite avec les outils de build.

La difference fondamentale

Un import classique peut emettre du code JavaScript a l'execution. Un import type est entierement efface par le compilateur TypeScript — il n'existe plus dans le JS produit.

// import classique — peut emettre du JS si utilise comme valeur
import { Component, OnInit } from '@angular/core';
import { User } from './models/user.model';

// import type — TOUJOURS efface, jamais emis en JS
// Utiliser quand on n'utilise l'import QUE comme type TypeScript
import type { UserProfile } from './models/user-profile.model';
import type { ApiResponse } from './interfaces/api.interface';
// Exemple concret : quand utiliser import type ?
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

// UserProfile n'est utilise que comme annotation de type
// Le compilateur l'effacera automatiquement du JS produit
import type { UserProfile } from '../models/user-profile.model';

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  // UserProfile apparait uniquement dans la signature de type
  // Il sera efface a la compilation → pas d'import inutile en JS
  getUser(id: number): Observable<UserProfile> {
    return this.http.get<UserProfile>(`/api/users/${id}`);
  }
}

verbatimModuleSyntax : le mode strict

Depuis TypeScript 5.0, l'option verbatimModuleSyntax force l'utilisation explicite de import type pour tous les imports qui ne servent que de types. Le compilateur genere une erreur si un import de valeur est utilise uniquement comme type.

// tsconfig.json — activation de verbatimModuleSyntax
{
  "compilerOptions": {
    // Force l'utilisation de "import type" pour les imports purement typages
    // Recommande avec "module": "ESNext" et les bundlers modernes
    "verbatimModuleSyntax": true,

    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}
// AVEC verbatimModuleSyntax: true

// ERREUR — UserProfile est un type, il faut "import type"
// import { UserProfile } from './models/user-profile.model'; // ❌

// CORRECT — import type explicite
import type { UserProfile } from './models/user-profile.model'; // ✅

// CORRECT — Component est utilise comme valeur (decorateur)
import { Component } from '@angular/core'; // ✅
Note : verbatimModuleSyntax remplace les anciennes options importsNotUsedAsValues et preserveValueImports deprecies depuis TypeScript 5.0. Privilegiez la nouvelle option dans les nouveaux projets.

Import type inline : la syntaxe compacte

// Import type inline : melanger valeurs et types dans un seul import
// Utile quand on importe depuis le meme module
import { Component, type OnInit, type OnDestroy } from '@angular/core';

@Component({ selector: 'app-root', template: '' })
export class AppComponent implements OnInit, OnDestroy {
  // OnInit et OnDestroy sont effaces ; Component reste dans le JS
  ngOnInit(): void { /* ... */ }
  ngOnDestroy(): void { /* ... */ }
}

Barrel exports : index.ts et tree-shaking

Un barrel export est un fichier index.ts qui re-exporte les membres d'un dossier depuis un point d'entree unique. C'est un pattern tres populaire dans les projets Angular, mais il comporte des pieges importants si mal utilise.

Principe du barrel export

// Structure du dossier src/app/core/models/
// ├── user.model.ts
// ├── product.model.ts
// ├── order.model.ts
// └── index.ts  ← barrel export

// index.ts — re-exporte tout le contenu du dossier
// Chaque re-export rend les types publics depuis ce dossier
export { User, UserRole } from './user.model';
export { Product, ProductCategory } from './product.model';
export { Order, OrderStatus } from './order.model';

// Re-export de types uniquement (recommande avec verbatimModuleSyntax)
export type { UserProfile } from './user-profile.model';
// AVANT barrel — imports longs et repetitifs
import { User } from '../core/models/user.model';
import { Product } from '../core/models/product.model';
import { Order } from '../core/models/order.model';

// APRES barrel — import depuis le point d'entree unique
// Plus lisible, plus facile a maintenir
import { User, Product, Order } from '../core/models';

Avantages des barrel exports

  • Imports plus courts et plus lisibles dans les fichiers consommateurs.
  • API publique d'un module clairement definie dans un seul fichier.
  • Refactoring facilite : deplacer un fichier sans changer les imports externes.
  • Encapsulation : les fichiers non-exportes restent prives au dossier.

Inconvenients et impact sur le tree-shaking

Les barrels peuvent degrader les performances si utilises sans discernement. Un bundler comme esbuild ou Webpack doit analyser tout le barrel pour savoir ce qui est utilise, ce qui ralentit le build et peut inclure du code inutile.

// PROBLEME : barrel qui importe tout, y compris des modules lourds
// Si un seul consommateur importe "UserService",
// le bundler charge AUSSI HeavyChartModule si le barrel le re-exporte

// src/app/shared/index.ts (barrel trop large — a eviter)
export * from './components/button.component';
export * from './components/modal.component';
export * from './services/user.service';
// Attention : si HeavyChartModule importe des libs tierces volumineuses,
// tout consommateur du barrel les chargera potentiellement
export * from './charts/heavy-chart.module';
// SOLUTION : barrels granulaires et specifiques
// Separer les barrels par domaine fonctionnel

// src/app/shared/components/index.ts
export * from './button.component';
export * from './modal.component';
// Ne pas melanger composants UI et services lourds

// src/app/shared/services/index.ts
export * from './user.service';
export * from './auth.service';

// Chaque consommateur n'importe que ce dont il a besoin
import { ButtonComponent } from '@shared/components';
import { UserService } from '@shared/services';
A retenir : les barrel exports ameliorent l'ergonomie mais peuvent casser le tree-shaking si un seul barrel re-exporte des dizaines de modules. Privilegier des barrels par sous-domaine (components, services, models) plutot qu'un mega-index a la racine.

export * vs export { } : quelle difference ?

// export * — re-exporte TOUT ce qui est exporte depuis le module source
// Simple mais peut exposer des membres non intentionnels
export * from './user.model';

// export { } nomme — re-exporte UNIQUEMENT les membres specifies
// Plus explicite, meilleure maitrise de l'API publique (recommande)
export { User, UserRole } from './user.model';

// export * as namespace — re-exporte sous un namespace
// Utile pour eviter les collisions de noms
export * as UserModels from './user.model';

Dependances circulaires : detection et prevention

Une dependance circulaire survient quand le module A importe B, et que B importe A (directement ou indirectement). Ce pattern provoque des valeurs undefined au runtime et des bugs difficiles a diagnostiquer.

Exemple de dependance circulaire

// PROBLEME : dependance circulaire entre deux services

// src/app/core/services/user.service.ts
import { Injectable } from '@angular/core';
// UserService importe OrderService
import { OrderService } from './order.service';

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private orderService: OrderService) {}

  getUserOrders(userId: number) {
    return this.orderService.getByUser(userId);
  }
}

// src/app/core/services/order.service.ts
import { Injectable } from '@angular/core';
// OrderService importe UserService → CYCLE !
import { UserService } from './user.service';

@Injectable({ providedIn: 'root' })
export class OrderService {
  constructor(private userService: UserService) {}

  getByUser(userId: number) {
    // Au moment de l'execution, l'un des deux peut etre "undefined"
    return this.userService.getProfile(userId);
  }
}

Detecter les cycles avec des outils

Deux outils sont particulierement efficaces pour detecter les dependances circulaires dans un projet Angular : madge et le plugin circular-dependency-plugin.

# Installation de madge (outil en ligne de commande)
npm install --save-dev madge

# Detecter toutes les dependances circulaires dans src/
npx madge --circular --extensions ts src/

# Generer un graphe visuel des dependances (format SVG)
# Necessite graphviz installe sur le systeme
npx madge --image dependency-graph.svg --extensions ts src/
// Pour un projet Angular avec esbuild, detecter les cycles
// via une configuration TypeScript stricte

// tsconfig.json — options qui aident a detecter les problemes
{
  "compilerOptions": {
    // Detecte les imports jamais utilises (symptome d'un mauvais design)
    "noUnusedLocals": true,
    "noUnusedParameters": true,

    // Force la coherence entre les modules
    "isolatedModules": true,

    // Empêche l'utilisation de valeurs potentiellement undefined
    "strictNullChecks": true
  }
}

Strategies pour eliminer les cycles

La solution la plus efficace est d'extraire les types partages dans un module tiers sans dependances.

// SOLUTION 1 : extraire les types dans un module independant

// src/app/core/models/user.types.ts — module sans dependances
// Ce module ne depend d'aucun autre module metier
export interface UserSummary {
  id: number;
  name: string;
  email: string;
}

// src/app/core/services/user.service.ts
import { Injectable } from '@angular/core';
// Plus d'import circulaire : on importe uniquement le type
import type { UserSummary } from '../models/user.types';

@Injectable({ providedIn: 'root' })
export class UserService {
  // Retourne un type simple, pas OrderService
  getProfile(userId: number): Observable<UserSummary> {
    return this.http.get<UserSummary>(`/api/users/${userId}`);
  }
}
// SOLUTION 2 : inverser la dependance via un token d'injection

// src/app/core/tokens/order-provider.token.ts
import { InjectionToken } from '@angular/core';

// Interface sans import circulaire : contrat entre les deux services
export interface IOrderProvider {
  getByUser(userId: number): Observable<Order[]>;
}

// Token d'injection pour le service
export const ORDER_PROVIDER = new InjectionToken<IOrderProvider>('OrderProvider');

// user.service.ts — injecte le token, pas la classe concrete
import { Inject, Injectable, Optional } from '@angular/core';
import { ORDER_PROVIDER, IOrderProvider } from '../tokens/order-provider.token';

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(
    @Optional() @Inject(ORDER_PROVIDER) private orderProvider: IOrderProvider | null
  ) {}
}
Astuce : les barrels (index.ts) peuvent creer involontairement des cycles si deux modules du meme dossier s'importent mutuellement via le barrel. Toujours verifier que les fichiers d'un meme dossier s'importent directement, pas via leur propre index.ts.

Path aliasing avec tsconfig paths dans Angular

Les alias de chemins permettent de remplacer les chemins relatifs longs (../../../core/services) par des chemins semantiques (@core/services). C'est un outil puissant pour l'organisation d'un projet Angular a grande echelle.

Configuration dans tsconfig.json

// tsconfig.json — configuration des alias de chemins
{
  "compilerOptions": {
    // baseUrl est obligatoire pour utiliser les paths
    // Il definit la racine a partir de laquelle les alias sont resolus
    "baseUrl": ".",

    // Alias de chemins : chaque cle est un pattern, la valeur un tableau de chemins
    "paths": {
      // @core/* → remplace "src/app/core/*"
      "@core/*":     ["src/app/core/*"],

      // @shared/* → remplace "src/app/shared/*"
      "@shared/*":   ["src/app/shared/*"],

      // @features/* → remplace "src/app/features/*"
      "@features/*": ["src/app/features/*"],

      // @env/* → remplace les fichiers d'environnement
      "@env/*":      ["src/environments/*"],

      // Alias exact (sans wildcard) pour un fichier specifique
      "@app-config": ["src/app/app.config"]
    }
  }
}

Utilisation dans le code Angular

// AVANT alias — chemins relatifs profonds et fragiles
// Si on deplace ce fichier, tous ces imports cassent
import { AuthService }    from '../../../core/services/auth.service';
import { UserModel }      from '../../../core/models/user.model';
import { ButtonComponent } from '../../shared/components/button/button.component';
import { environment }    from '../../../../environments/environment';

// APRES alias — chemins absolus semantiques
// Ces imports fonctionnent quelle que soit la profondeur du fichier
import { AuthService }    from '@core/services/auth.service';
import { UserModel }      from '@core/models/user.model';
import { ButtonComponent } from '@shared/components/button/button.component';
import { environment }    from '@env/environment';

Configurer Angular pour respecter les alias

Avec Angular CLI et esbuild (Angular 17+), les alias tsconfig paths sont automatiquement pris en charge. Pour les anciennes versions avec Webpack, une configuration supplementaire peut etre necessaire.

// angular.json — aucune configuration supplementaire necessaire avec esbuild
// Les alias tsconfig.paths sont resolus automatiquement par @angular-devkit

// VERIFICATION : s'assurer que tsconfig.app.json herite du tsconfig.json racine
// tsconfig.app.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    // Peut surcharger certaines options specifiques a la compilation de l'app
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "src/main.ts"
  ],
  "include": [
    "src/**/*.d.ts"
  ]
}

// tsconfig.spec.json herite aussi du tsconfig.json racine
// Les tests Jest/Karma ont donc acces aux memes alias de chemins
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "types": ["jasmine"]
  }
}

Alias avec Jest (configuration complementaire)

// jest.config.ts — configuration Jest pour resoudre les alias
// Jest ne lit pas tsconfig.json directement, il faut mapper les alias
import type { Config } from 'jest';

const config: Config = {
  // Mapping des alias TypeScript vers les chemins reels
  // La cle est une regex, la valeur un tableau de chemins
  moduleNameMapper: {
    // Resout @core/xxx vers src/app/core/xxx
    '^@core/(.*)$':     '/src/app/core/$1',
    '^@shared/(.*)$':   '/src/app/shared/$1',
    '^@features/(.*)$': '/src/app/features/$1',
    '^@env/(.*)$':      '/src/environments/$1',
  },
  preset: 'jest-preset-angular',
};

export default config;
A retenir : les alias de chemins sont resolus par TypeScript a la compilation, mais les bundlers et les runners de tests ont leurs propres systemes de resolution. Il faut toujours configurer les deux separement.

Module resolution : node16, bundler, classic

L'option moduleResolution dans tsconfig.json indique a TypeScript comment trouver les fichiers correspondant a un import. Ce choix a un impact direct sur la compatibilite avec Node.js, les bundlers et les packages tiers.

Les quatre strategies disponibles

Valeur Usage recommande Compatibilite
classic Projets tres anciens uniquement TypeScript 1.x uniquement
node (ou node10) Node.js CJS legacy Node.js < 12, anciens projets
node16 / nodenext Node.js ESM natif Node.js 16+, ESM strict
bundler Vite, esbuild, Webpack, Rollup Angular 17+, projets modernes

bundler : le choix optimal pour Angular

Depuis TypeScript 5.0, "moduleResolution": "bundler" est la valeur recommandee pour les projets utilisant un bundler moderne comme esbuild (Angular 17+). Elle autorise les imports sans extension tout en supportant les exports maps des packages npm.

// tsconfig.json — configuration optimale pour Angular 17+ avec esbuild
{
  "compilerOptions": {
    // bundler : mode concu pour les bundlers modernes (esbuild, Vite, Webpack 5)
    // Autorise les imports sans extension (ex: import './utils' sans '.js')
    // Supporte les "exports" maps dans package.json
    "moduleResolution": "bundler",

    // ESNext preserve les import/export pour le bundler
    "module": "ESNext",

    // Necessite module: ESNext ou module: Preserve
    // Force l'usage de "import type" pour les types purs
    "verbatimModuleSyntax": true,

    // ES2022 cible les navigateurs modernes et Node.js 16+
    "target": "ES2022"
  }
}

node16 : quand l'utiliser ?

node16 est la bonne option pour les bibliotheques ou outils Node.js qui publient en ESM natif. Il impose des regles strictes : les imports doivent inclure l'extension .js et le champ type: "module" doit etre present dans package.json.

// package.json — requis pour node16 ESM
{
  "name": "ma-librairie",
  // Declare explicitement le module comme ESM
  "type": "module",
  "exports": {
    // Point d'entree ESM
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  }
}

// Avec node16, les imports doivent inclure l'extension .js
// meme si le fichier source est un .ts
// (TypeScript resout automatiquement .js → .ts)
import { MyFunction } from './utils.js'; // ✅ avec node16
// import { MyFunction } from './utils'; // ❌ invalide avec node16
Note : nodenext est identique a node16 mais suit les evolutions futures de Node.js. Pour les nouvelles librairies Node.js ESM, privilegier nodenext.

Fichiers de declaration .d.ts et ambient modules

Les fichiers .d.ts sont des fichiers de declaration TypeScript : ils decrivent les types d'un module sans contenir de code executable. Ils permettent d'ajouter le typage TypeScript a des librairies JavaScript ou de declarer des modules speciaux.

Quand creer un fichier .d.ts ?

  • Quand une librairie JavaScript tierce n'a pas de types (@types/xxx inexistant).
  • Pour declarer des modules non-TypeScript (images, SVG, CSS Modules, JSON).
  • Pour augmenter les types d'une librairie existante (declaration merging).
  • Pour declarer des variables globales injectees par l'environnement (ex: __ENV__, window.APP_CONFIG).

Ambient module : typer une librairie sans types

// src/types/legacy-lib.d.ts
// Declare un module JavaScript sans types disponibles
// "declare module" cree un module ambient TypeScript
declare module 'legacy-chart-lib' {
  // Declare les exports du module avec leurs types
  export interface ChartOptions {
    type: 'bar' | 'line' | 'pie';
    data: number[];
    labels: string[];
    colors?: string[];
  }

  // Declare la fonction principale de la librairie
  export function createChart(
    container: HTMLElement,
    options: ChartOptions
  ): { destroy(): void; update(data: number[]): void };
}

// Wildcard ambient module — accepte tous les imports d'un pattern
// Utile pour les fichiers SVG, images, CSS Modules
declare module '*.svg' {
  // Declare que les imports SVG retournent une URL string
  const content: string;
  export default content;
}

declare module '*.png' {
  const content: string;
  export default content;
}

Augmentation de module (declaration merging)

// src/types/express-augmentation.d.ts
// Augmente les types Express pour ajouter une propriete "user" sur Request
// C'est une technique avancee pour etendre les types d'une librairie tierce
import { User } from '../app/core/models/user.model';

// "declare module" avec le nom exact du module augmente
declare module 'express-serve-static-core' {
  interface Request {
    // Ajoute la propriete user au type Request d'Express
    // Disponible sur toutes les routes apres le middleware d'authentification
    user?: User;
  }
}

Variables globales et environnement

// src/types/global.d.ts
// Declare des variables globales injectees par le bundler ou l'environnement
// Ces declarations doivent etre dans un fichier .d.ts inclus par tsconfig

// Variables injectees par esbuild ou Vite (ex: define dans vite.config.ts)
declare const __APP_VERSION__: string;
declare const __BUILD_DATE__: string;
declare const __IS_PRODUCTION__: boolean;

// Extension de l'objet Window pour des proprietes personnalisees
interface Window {
  // Configuration injectee depuis le HTML ou le serveur
  APP_CONFIG?: {
    apiUrl: string;
    featureFlags: Record<string, boolean>;
  };
}

// src/types/environment.d.ts
// Declare l'interface des fichiers d'environnement Angular
export interface Environment {
  production: boolean;
  apiUrl: string;
  wsUrl: string;
}

Inclure les .d.ts dans tsconfig

// tsconfig.json — s'assurer que les fichiers .d.ts sont inclus
{
  "compilerOptions": {
    // typeRoots : dossiers de declarations de types
    // TypeScript cherche ici les declarations @types/xxx
    "typeRoots": [
      "./node_modules/@types",
      "./src/types"  // Nos declarations personnalisees
    ]
  },
  // include : fichiers sources TypeScript a compiler
  // Les .d.ts dans src/types/ sont automatiquement inclus
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts"
  ]
}

Structure d'un projet Angular avec alias de chemins

Une architecture Angular bien organisee avec des alias de chemins rend le code plus lisible, plus maintenable et plus facile a faire evoluer. Voici une structure eprouvee pour un projet de taille moyenne a grande.

Structure de dossiers recommandee

src/
├── app/
│   ├── core/               ← Alias : @core
│   │   ├── guards/
│   │   │   ├── auth.guard.ts
│   │   │   └── index.ts    ← Barrel
│   │   ├── interceptors/
│   │   │   ├── auth.interceptor.ts
│   │   │   ├── error.interceptor.ts
│   │   │   └── index.ts    ← Barrel
│   │   ├── models/
│   │   │   ├── user.model.ts
│   │   │   ├── api-response.model.ts
│   │   │   └── index.ts    ← Barrel
│   │   ├── services/
│   │   │   ├── auth.service.ts
│   │   │   ├── user.service.ts
│   │   │   └── index.ts    ← Barrel
│   │   └── index.ts        ← Barrel racine core
│   ├── shared/             ← Alias : @shared
│   │   ├── components/
│   │   │   ├── button/
│   │   │   ├── modal/
│   │   │   └── index.ts    ← Barrel
│   │   ├── directives/
│   │   │   └── index.ts
│   │   ├── pipes/
│   │   │   └── index.ts
│   │   └── index.ts        ← Barrel racine shared
│   └── features/           ← Alias : @features
│       ├── dashboard/
│       │   ├── components/
│       │   ├── services/
│       │   └── index.ts
│       └── products/
│           ├── components/
│           ├── services/
│           └── index.ts
├── environments/           ← Alias : @env
│   ├── environment.ts
│   └── environment.prod.ts
└── types/                  ← Declarations .d.ts globales
    ├── global.d.ts
    └── modules.d.ts

tsconfig.json complet pour cette structure

// tsconfig.json — configuration complete pour un projet Angular moderne
{
  "compilerOptions": {
    // Cibles et modules
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "dom", "dom.iterable"],

    // Qualite et securite du code
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,

    // Interoperabilite avec les modules CJS de node_modules
    "esModuleInterop": true,

    // Force "import type" pour les types purs
    "verbatimModuleSyntax": true,

    // Declarations de types
    "typeRoots": ["./node_modules/@types", "./src/types"],

    // Alias de chemins
    "baseUrl": ".",
    "paths": {
      "@core":          ["src/app/core/index.ts"],
      "@core/*":        ["src/app/core/*"],
      "@shared":        ["src/app/shared/index.ts"],
      "@shared/*":      ["src/app/shared/*"],
      "@features":      ["src/app/features"],
      "@features/*":    ["src/app/features/*"],
      "@env/*":         ["src/environments/*"]
    }
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

Exemple concret d'utilisation dans un composant

// src/app/features/dashboard/components/dashboard.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { AsyncPipe, DatePipe } from '@angular/common';

// Imports via alias — lisibles et independants de la profondeur
import { AuthService, UserService } from '@core/services';
import type { User, UserProfile } from '@core/models';
import { ButtonComponent, ModalComponent } from '@shared/components';
import { TruncatePipe } from '@shared/pipes';
import { environment } from '@env/environment';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  // Imports des composants et pipes utilises dans le template
  imports: [AsyncPipe, DatePipe, ButtonComponent, ModalComponent, TruncatePipe],
  templateUrl: './dashboard.component.html'
})
export class DashboardComponent implements OnInit {
  // Injection via la fonction inject() (Angular 14+)
  private authService = inject(AuthService);
  private userService = inject(UserService);

  // Signal pour l'utilisateur courant
  currentUser: UserProfile | null = null;

  ngOnInit(): void {
    // Recupere le profil depuis le service
    this.userService.getProfile(this.authService.userId)
      .subscribe(profile => {
        this.currentUser = profile;
      });
  }
}
A retenir : les alias de chemins rendent le code lisible a n'importe quelle profondeur de l'arborescence. Combiner des barrels granulaires et des alias semantiques est la cle d'une architecture Angular maintenable sur le long terme.

Accessibilite et responsive : bonnes pratiques

Une bonne organisation du code impact directement la qualite des composants produits. Des services correctement isoles facilitent la mise en place de composants accessibles et responsives.

  • Les composants @shared/components doivent toujours inclure les attributs ARIA (aria-label, role, aria-expanded).
  • Les modeles @core/models peuvent inclure des proprietes d'accessibilite (ariaLabel?: string).
  • Utiliser les grilles Bootstrap 4 (col-12 col-md-6 col-lg-4) dans les templates des composants partages.
  • Tester les composants partages avec des lecteurs d'ecran pour garantir leur accessibilite.

Mini-projet appliqué — monorepo Angular + NestJS avec alias partagés

Le cas concret qui justifie à lui seul tous les patterns vus : un monorepo Nx avec une application Angular 17+, une API NestJS, et une librairie partagée contenant les types TypeScript (DTO, modèles, enums) consommés des deux côtés. Sans alias ni barrel, ce setup deviendrait illisible en quelques semaines.

1. Structure du monorepo

my-saas/
├── apps/
│   ├── web/                    # Angular 17+ standalone
│   │   ├── src/app/...
│   │   ├── tsconfig.json
│   │   └── project.json
│   └── api/                    # NestJS
│       ├── src/...
│       ├── tsconfig.json
│       └── project.json
├── libs/
│   ├── shared-types/           # DTO + modèles partagés
│   │   ├── src/
│   │   │   ├── dto/
│   │   │   ├── models/
│   │   │   ├── enums/
│   │   │   └── index.ts        # barrel public
│   │   └── tsconfig.json
│   └── ui-kit/                 # Composants Angular réutilisables
│       └── src/index.ts
├── tsconfig.base.json          # alias centralisés
└── nx.json

2. Alias centralisés — tsconfig.base.json

Une seule définition d'alias, héritée par tous les projets du monorepo. Cela évite la divergence entre frontend et backend. Pour les patterns de configuration TS, voir notre guide sur le strict mode et tsconfig.

// tsconfig.base.json (à la racine du monorepo)
{
    "compilerOptions": {
        "strict": true,
        "verbatimModuleSyntax": true,
        "moduleResolution": "bundler",
        "target": "ES2022",
        "lib": ["ES2022", "DOM"],
        "paths": {
            "@my-saas/shared-types":        ["libs/shared-types/src/index.ts"],
            "@my-saas/shared-types/*":      ["libs/shared-types/src/*"],
            "@my-saas/ui-kit":              ["libs/ui-kit/src/index.ts"],
            "@my-saas/ui-kit/*":            ["libs/ui-kit/src/*"]
        }
    }
}

3. Barrel de la lib partagée — surface API contrôlée

Le barrel libs/shared-types/src/index.ts expose UNIQUEMENT ce qui doit être consommé en externe. Les détails d'implémentation restent privés au module.

// libs/shared-types/src/index.ts
// Modèles métier
export type { User, Order, Product } from './models/index.js';

// DTO (API contracts)
export type {
    CreateUserDto,
    UpdateUserDto,
    CreateOrderDto,
} from './dto/index.js';

// Enums (runtime + types)
export { UserRole, OrderStatus, Currency } from './enums/index.js';

// Helpers de validation (Zod schemas)
export {
    UserSchema,
    OrderSchema,
    CreateUserSchema,
} from './schemas/index.js';
Gain mesuré : sur un projet SaaS de 18 mois, ce setup a permis de partager ~40 types et 15 schémas Zod entre front et back avec zéro duplication. La synchronisation des contrats API se fait en éditant un seul fichier — la TypeScript compilation casse instantanément côté front si le back change un DTO.

4. Consommation côté Angular — imports lisibles

// apps/web/src/app/features/users/user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import type { User, CreateUserDto, UpdateUserDto } from '@my-saas/shared-types';
import { UserSchema } from '@my-saas/shared-types';
import { CardComponent } from '@my-saas/ui-kit';

@Injectable({ providedIn: 'root' })
export class UserService {
    private http = inject(HttpClient);

    async create(dto: CreateUserDto): Promise<User> {
        const raw = await this.http.post('/api/users', dto).toPromise();
        return UserSchema.parse(raw); // validation runtime
    }
}

5. Consommation côté NestJS — mêmes alias

// apps/api/src/users/users.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import type { CreateUserDto, User } from '@my-saas/shared-types';
import { CreateUserSchema } from '@my-saas/shared-types';
import { UsersService } from './users.service.js';

@Controller('users')
export class UsersController {
    constructor(private readonly users: UsersService) {}

    @Post()
    async create(@Body() body: unknown): Promise<User> {
        // Validation Zod côté serveur — même schéma que le client
        const dto: CreateUserDto = CreateUserSchema.parse(body);
        return this.users.create(dto);
    }
}

6. Pièges critiques à éviter dans un monorepo

  • Importer du runtime Angular dans la lib shared-types — la lib doit rester pure (zéro dépendance Angular/Node), sinon le backend casse à l'import.
  • Re-exporter sans réfléchir — un export * from './internal' casse l'encapsulation. Toujours nommer explicitement.
  • Imports relatifs au lieu des aliasimport from '../../../libs/shared-types' bypass la résolution du monorepo et casse au déplacement de fichiers.
  • Cycle entre shared-types et ui-kit — détectable avec npx madge --circular libs/. À traiter dès la première itération.

Pour aller plus loin sur les patterns avancés (génériques pour Repository typé, mapped types pour les DTO), lire le guide mapped types + template literal types, et pour pousser le typage à l'extrême (URL typées, parsing à la compilation), voir infer et conditional types avancés.

Conclusion

Maitriser les modules TypeScript — ES Modules, import type, barrel exports, alias de chemins et fichiers de declaration — est ce qui separe un projet Angular maintenable d'un projet qui accumule de la dette technique. Ces outils, bien utilises ensemble, reduisent la complexite des imports, ameliorent le tree-shaking et rendent l'architecture lisible pour toute l'equipe.

Commencer par configurer verbatimModuleSyntax et moduleResolution: "bundler" dans le tsconfig.json, puis introduire progressivement les alias de chemins et les barrels granulaires. Surveiller les dependances circulaires avec madge des le debut du projet pour eviter les surprises.

A retenir : les barrel exports simplifient les imports mais peuvent casser le tree-shaking si trop larges. Privilegier des barrels par domaine fonctionnel (@core/services, @shared/components) plutot qu'un index global qui re-exporte tout.

Partager