Où placer une interface ou un type ? La réponse dépend d’un seul critère : qui l’utilise. Un type doit rester le plus près possible du code qui l’utilise et n’être « remonté » que lorsqu’un nouveau consommateur apparaît plus loin.
- Utilisé par un seul fichier → dans ce fichier.
- Le type d’un input/output → exporté depuis le fichier du composant.
- Utilisé par plusieurs fichiers d’une fonctionnalité →
features/x/models/. - Utilisé par plusieurs fonctionnalités → la fonctionnalité propriétaire + une API publique (ou
shared/modelss’il est vraiment transversal). - Vient du serveur → un DTO séparé + un mapper.
Un type utilisé par un seul fichier est un détail d’implémentation. Gardé à proximité, il est facile à trouver et à modifier.
// product-filters.ts
interface FilterChip { // utilisé uniquement ici
label: string;
active: boolean;
}
@Component({ ... })
export class ProductFilters { ... }En ligne : quand le type est un détail d’implémentation
// ✓ Un type utilisé uniquement par ce composant → dans le même fichier
interface SortOption {
value: 'price' | 'rating' | 'name';
label: string;
}
@Component({ selector: 'app-sort-menu', /* ... */ })
export class SortMenu {
protected readonly options: SortOption[] = [
{ value: 'price', label: 'Prix' },
{ value: 'rating', label: 'Note' },
];
}DTO vs modèle de domaine
Le DTO est le contrat avec le serveur : snake_case, dates sous forme de chaînes, champs null, prix en centimes. Le modèle est ce dont l’UI a besoin. Entre les deux se trouve un mapper, appliqué une seule fois, à la frontière (dans data-access). Les composants ne voient jamais de DTO.
// models/product-dto.ts — EXACTEMENT la forme venant du serveur
export interface ProductDto {
id: number;
product_name: string;
price_cents: number;
stock: number;
created_at: string; // ISO string
discount: number | null;
}
// models/product.ts — ce dont l’UI a besoin
export interface Product {
id: number;
name: string;
price: number; // unités monétaires, pas des centimes
inStock: boolean; // dérivé
createdAt: Date;
discount?: number; // pas de null
}
// models/product-mapper.ts — le seul endroit qui connaît les deux
export function toProduct(dto: ProductDto): Product {
return {
id: dto.id,
name: dto.product_name,
price: dto.price_cents / 100,
inStock: dto.stock > 0,
createdAt: new Date(dto.created_at),
...(dto.discount !== null && { discount: dto.discount }),
};
}{
"id": 1,
"name": "Ana Popescu",
"email": "ana@atlas.dev",
"role": "admin",
"city": "Cluj-Napoca",
"joined_at": "2021-03-14"
}- L’API a d’autres conventions (snake_case, montants en centimes, dates sous forme de chaînes).
- L’API n’est pas contrôlée par votre équipe ou change souvent.
- L’UI a besoin de champs dérivés (
inStock,fullName). - Une grande application avec de nombreux écrans sur le même type.
- CRUD simple, l’API (ou un BFF) est conçue exactement pour cette UI.
- Les types sont générés depuis OpenAPI et déjà « propres ».
- Un petit prototype / panneau interne : le mapping ne fait qu’ajouter du code.
type ou interface ?
// interface : formes d’objets (extensible, messages d’erreur clairs)
export interface User {
id: number;
name: string;
}
export interface Admin extends User {
permissions: string[];
}
// type : unions, intersections, types dérivés, fonctions
export type Role = 'admin' | 'editor' | 'viewer';
export type Id = number | string;
export type UserPreview = Pick<User, 'id' | 'name'>;
export type UserPatch = Partial<Omit<User, 'id'>>;
export type Comparator<T> = (a: T, b: T) => number;
export type LoadState<T> = { status: 'loading' } | { status: 'ok'; data: T } | { status: 'error'; error: string };Enum ou union ?
// ✓ Une union de chaînes + un objet `as const` plutôt qu’un enum
export const ORDER_STATUSES = ['pending', 'paid', 'shipped', 'cancelled'] as const;
export type OrderStatus = (typeof ORDER_STATUSES)[number]; // 'pending' | 'paid' | ...
export const ORDER_LABELS: Record<OrderStatus, string> = {
pending: 'En attente',
paid: 'Payé',
shipped: 'Expédié',
cancelled: 'Annulé',
}; // si vous ajoutez un statut et oubliez son libellé → erreur de compilation
// Dans le template : @for (s of statuses; track s) { ... } — facile à parcourir, sans Object.values(Enum)Nommage
// ✓ Des noms clairs, sans préfixes/suffixes inutiles
interface Product {} // pas IProduct, pas ProductModel, pas ProductInterface
interface ProductDto {} // la forme venant de l’API
interface CreateProductRequest {} // le corps du POST
interface ProductFilters {} // état de l’UI
type ProductId = number; // alias sémantique (optionnel)
// ✗ Des interfaces pour les services « parce que »
interface IProductService { list(): Observable<Product[]>; } // n’apporte rien
// Envie de plusieurs implémentations / mocks ? → une classe abstraite comme token (voir « inject() & @Service() »)Dérivez, ne réécrivez pas
// Dérivez les types depuis la source de vérité au lieu de les réécrire
type UserRow = Pick<User, 'id' | 'name' | 'email'>;
type ProductForm = Omit<Product, 'id' | 'createdAt'>;
type Store = InstanceType<typeof ProductsStore>;
type CartItem = ReturnType<CartStore['items']>[number];
// Un schéma de validation = type + validation à l’exécution, une seule source
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.email() });
type User = z.infer<typeof UserSchema>;
// Types générés depuis OpenAPI (openapi-typescript, orval, ng-openapi-gen)
import type { components } from './generated/api';
type ProductDto = components['schemas']['Product'];La conversion des DTO en modèles se fait dans la couche API : HttpClient, interceptors et couche d’API. Les unions discriminées sont aussi la base de Machines à états avec les signals.
Pièges fréquents
- Un « models/ » global avec tous les types de l’application : le même anti-pattern que les dossiers par type.
- Des DTO dans les composants : un changement d’API se propage dans toute l’application.
anyà la frontière (http.get<any>) : vous perdez tous les bénéfices. Donnez le type du DTO.- Les types TypeScript comme validation : TypeScript ne vérifie pas la réponse du serveur à l’exécution. Pour les données critiques, validez (Zod / Valibot dans
parse). - Des interfaces pour tout (
IUserService) : de la complexité sans bénéfice.