Wohin gehört ein interface oder ein type? Die Antwort hängt von einer einzigen Sache ab: wer ihn verwendet. Ein Typ sollte so nah wie möglich an dem Code bleiben, der ihn nutzt, und erst „hochgezogen“ werden, wenn weiter entfernt ein neuer Konsument auftaucht.
- Von einer Datei verwendet → in dieser Datei.
- Typ eines Inputs/Outputs → aus der Datei der Komponente exportiert.
- Von mehreren Dateien eines Features verwendet →
features/x/models/. - Von mehreren Features verwendet → das Feature, dem er gehört, + eine öffentliche API (oder
shared/models, wenn er wirklich übergreifend ist). - Kommt vom Server → ein eigenes DTO + ein Mapper.
Ein Typ, der nur von einer Datei genutzt wird, ist ein Implementierungsdetail. Nah am Code gehalten, ist er leicht zu finden und zu ändern.
// product-filters.ts
interface FilterChip { // wird nur hier verwendet
label: string;
active: boolean;
}
@Component({ ... })
export class ProductFilters { ... }Inline: wenn der Typ ein Implementierungsdetail ist
// ✓ Ein Typ, der nur von dieser Komponente genutzt wird → in derselben Datei
interface SortOption {
value: 'price' | 'rating' | 'name';
label: string;
}
@Component({ selector: 'app-sort-menu', /* ... */ })
export class SortMenu {
protected readonly options: SortOption[] = [
{ value: 'price', label: 'Preis' },
{ value: 'rating', label: 'Bewertung' },
];
}DTO vs. Domänenmodell
Das DTO ist der Vertrag mit dem Server: snake_case, Datumsangaben als Strings, null-Felder, Preise in Cent. Das Modell ist das, was die UI braucht. Dazwischen sitzt ein Mapper, der einmal an der Grenze angewendet wird (in data-access). Komponenten sehen nie DTOs.
// models/product-dto.ts — GENAU die Form vom Server
export interface ProductDto {
id: number;
product_name: string;
price_cents: number;
stock: number;
created_at: string; // ISO string
discount: number | null;
}
// models/product.ts — was die UI braucht
export interface Product {
id: number;
name: string;
price: number; // Währungseinheiten, nicht Cent
inStock: boolean; // abgeleitet
createdAt: Date;
discount?: number; // kein null
}
// models/product-mapper.ts — der einzige Ort, der beide kennt
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"
}- Die API hat andere Konventionen (snake_case, Geld in Cent, Datumsangaben als Strings).
- Die API wird nicht von deinem Team kontrolliert oder ändert sich oft.
- Die UI braucht abgeleitete Felder (
inStock,fullName). - Eine große Anwendung mit vielen Screens auf demselben Typ.
- Einfaches CRUD, die API (oder ein BFF) ist genau für diese UI gebaut.
- Die Typen werden aus OpenAPI generiert und sind bereits „sauber“.
- Ein kleiner Prototyp / internes Panel — das Mapping erzeugt nur zusätzlichen Code.
type oder interface?
// interface: Objektformen (erweiterbar, klare Fehlermeldungen)
export interface User {
id: number;
name: string;
}
export interface Admin extends User {
permissions: string[];
}
// type: Unions, Intersections, abgeleitete Typen, Funktionen
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 oder Union?
// ✓ Eine Union aus Strings + ein `as const`-Objekt statt eines Enums
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: 'Ausstehend',
paid: 'Bezahlt',
shipped: 'Versendet',
cancelled: 'Storniert',
}; // wenn du einen Status hinzufügst und sein Label vergisst → Compile-Fehler
// Im Template: @for (s of statuses; track s) { ... } — leicht zu iterieren, ohne Object.values(Enum)Namensgebung
// ✓ Klare Namen, ohne unnötige Präfixe/Suffixe
interface Product {} // nicht IProduct, nicht ProductModel, nicht ProductInterface
interface ProductDto {} // die Form aus der API
interface CreateProductRequest {} // der Body für POST
interface ProductFilters {} // UI-Zustand
type ProductId = number; // semantischer Alias (optional)
// ✗ Interfaces für Services „einfach so“
interface IProductService { list(): Observable<Product[]>; } // bringt nichts
// Mehrere Implementierungen / Mocks gewünscht? → eine abstrakte Klasse als Token (siehe „inject() & @Service()“)Ableiten statt neu schreiben
// Leite Typen aus der Single Source of Truth ab, statt sie neu zu schreiben
type UserRow = Pick<User, 'id' | 'name' | 'email'>;
type ProductForm = Omit<Product, 'id' | 'createdAt'>;
type Store = InstanceType<typeof ProductsStore>;
type CartItem = ReturnType<CartStore['items']>[number];
// Ein Validierungsschema = Typ + Laufzeitvalidierung, eine Quelle
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.email() });
type User = z.infer<typeof UserSchema>;
// Aus OpenAPI generierte Typen (openapi-typescript, orval, ng-openapi-gen)
import type { components } from './generated/api';
type ProductDto = components['schemas']['Product'];Das Mapping von DTOs auf Modelle gehört in die API-Schicht: HttpClient, Interceptors & die API-Schicht. Discriminated Unions sind auch die Basis für Zustandsautomaten mit Signals.
Häufige Stolperfallen
- Ein globales „models/“ mit allen Typen der Anwendung — dasselbe Anti-Pattern wie Ordner nach Typ.
- DTOs in Komponenten — eine API-Änderung zieht sich durch die ganze Anwendung.
anyan der Grenze (http.get<any>) — du verlierst alle Vorteile. Gib den Typ des DTOs an.- TypeScript-Typen als Validierung: TypeScript prüft die Antwort des Servers nicht zur Laufzeit. Validiere kritische Daten (Zod / Valibot in
parse). - Interfaces für alles (
IUserService) — Komplexität ohne Nutzen.