TypeScript ist nicht langsam – überdimensionierte Typen sind das Problem.
Allgemeine Suppen, vorzeitige Trocknung der Typen, optionale Flaggenzustände sowie typbezogene „Intelligenz“ verringern die Geschwindigkeit. Bevorzugen Sie einfache Schnittstellen, differenzierte Unionen sowie sorgfältig abgewogene tsc-Kosten.
Wie generische Suppen, konditionale Programmiertechniken und vorzeitige Abstraktion die Ausführungsgeschwindigkeit behindern.
In einer Sprint-Retrospektive gibt ein Junior-Engineer zu, dass das Hinzufügen eines optionalen Feldes zum bestehenden API-Payload vier Stunden in Anspruch nahm. Die IDE zeigt den Grund: eine Schnittstelle, die aus verschachtelten Bedingungen aufgebaut ist.
type ExtractNestedPayload<
T,
K extends keyof T,
U extends boolean = false
> =
T[K] extends (...args: any[]) => infer R
? R extends Promise<infer P>
? U extends true ? NonNullable<P> : P
: R
: T[K] extends Array<infer Item>
? Item
: never;
Vier Ebenen verschachtelter Bedingungen, drei generische Parameter sowie eine ternäre Kette, für die ein Whiteboard benötigt wird. Compilerfehler erscheinen als lange rote Hilfetexte:
Type 'ExtractNestedPayload<ApiResponses["users"], "data", true>'
is not assignable to type 'UserDTO'.
Types of property 'meta' are incompatible.Type 'unknown' is not assignable to type
'{ pagination: PaginationMeta }'.
Die übliche Beschwerde lautet: TypeScript verlangsamt das Team selbst. Normalerweise ist das nicht der Fall – überdimensionierte Abstraktionen schon. TypeScript sollte ursprünglich als pragmatischer Schutzmechanismus dienen: weniger Abstürze während der Ausführung und bessere Autocomplete-Funktionen. Irgendwann verwandelten viele Codebasen es jedoch in ein Rätsel: Stunden wurden mit mathematisch „perfekten“ Generics verbracht, nur um ein paar einfache Deklarationen zu vermeiden. Je prunkvoller das Typenmodell wird, desto weniger hilft es den Entwicklern, die Funktionen umsetzen. Ein Typ sollte die Absicht vermitteln. Wenn dessen Verständnis bedingte Typen, ungewöhnliche Inferenzmechanismen, mappierte Typen, distributive Unionen, rekursive Generics sowie eine Reihe von Hilfsfunktionen erfordert, ist die ursprüngliche Absicht verloren und ein weiteres System muss debuggt werden. Im Folgenden sind Muster aufgeführt, die in der Produktion zu Problemen führen, sowie sauberere Alternativen, die die Arbeitsgeschwindigkeit wiederherstellen.
1. Das Anti-Muster der generischen „Suppe“
Generics sind die Grundlage für Promise<T>, Array<T> und Map<K, V>. Probleme entstehen, wenn Flexibilität zu einem Zeichen für höhere Erfahrung wird. Mehr Generics bedeutet nicht automatisch etwas Besseres. Nehmen wir ein Tabellenkomponente:
// The Generic Soup Nightmare
interface TableProps<
TData,
TKey extends keyof TData,
TColumn extends ColumnDef<TData, any>,
TFilter extends Record<string, any> = Record<string, any>
> {
data: TData[];
keyExtractor: (item: TData) => TData[TKey];
columns: TColumn[];
initialFilter?: TFilter;
onRowClick?: (row: TData) => void;
}
Sie scheint unendlich wiederverwendbar zu sein: Datenstruktur, Zeilenkey, Spalten, Filter. Jede Abstraktion verursacht kognitive Kosten. Die Aufrufstellen zwingen den Compiler, mehrere miteinander verbundene Parameter abzuleiten. Ein kleiner Mismatch bei den Eigenschaften deutet nicht unbedingt auf die fehlerhafte Eigenschaft hin; die Ableitungen können sich im gesamten Graphen auswirken. Ein neuer Teamkollege muss lernen, warum TKey existiert, warum es keyof TData erweitert, wie Spalten- und Filter-Generics miteinander interagieren und was der Compiler tatsächlich abgeleitet hat. Das ist eine hohe Belastung für eine Tabellenkomponente.
Diese Folgen zeigen sich in langsameren Code-Reviews, längeren Einarbeitungszeiten sowie einer Kultur, in der nur ein oder zwei Personen es wagen, gemeinsam genutzte UI-Elemente zu ändern. Der Geschwindigkeitsverlust ist genauso sehr sozialer als auch technischer Natur: Teammitglieder hören auf, kleine Verbesserungen vorzuschlagen, weil sie die möglichen Konsequenzen fürchten. Wenn eine Tabellenerweiterung einen Design-Dokument benötigt, um ihre Generics zu erklären, hat sich diese Erweiterung bereits über das Problem hinausentwickelt, das sie ursprünglich lösen sollte.
Die Symptome in der Produktion sind langweilig und teuer. Schon eine einfache Umbenennung einer Eigenschaft löst Diagnosen aus, in denen unverwandte Typparameter erwähnt werden. Die Autocomplete-Funktion stockt, während der Sprachdienst das Generik-Graph neu bewertet. Die Zeit für die Typüberprüfungen im CI steigt stetig an, ohne dass jemand ein klareres Domänenmodell bereitstellt. Keine dieser Kosten taucht in Vergleichen wie „TypeScript gegen JavaScript“ auf – sie zeigen sich im Kalenderzeitraum.
Die konkrete Alternative
Wählen Sie einen Datenparameter sowie einfache Unterstützungsinterfaces:
// Simple, readable, and instant compiler diagnostics
interface TableColumn<T> {
header: string;
accessor: (item: T) => React.ReactNode;
width?: string;
}
interface DataTableProps<T> {
data: T[];
columns: TableColumn<T>[];
rowKey: (item: T) => string;
}
Das Komponente bleibt wiederverwendbar, ohne zu einem Typ-Orakel zu werden. Wiederverwendbar bedeutet nicht unendlich generisch.
Wiederverwendbar bedeutet nicht unendlich generisch.
Mannche Teams befürchten, dass die Vereinfachung von Generics zu Kopieren und Einfügen zwingt. In der Praxis sind zwei oder drei fokussierte Tabellenvarianten mit klaren Eigenschaften besser als eine universelle Komponente, die niemand ohne Ausprobieren instanzieren kann. Teilen Sie sich die Darstellungshilfen und das CSS; halten Sie die öffentlichen Eigenschaften einfach. Der Compiler weist dann auf das genaue fehlerhafte Feld hin, anstatt vier Inferenzvariablen in ein unknown-Dilemma zu führen.
2. Frühzeitiges DRY in Typdefinitionen
„Don’t Repeat Yourself“ ist bei Laufzeitcode nützlich, doch gefährlich, wenn es blind auf Typen angewandt wird. Wenn Teams ähnliche Felder sehen, leiten sie einen Typ aus einem anderen ab:
// Over-abstracted type derivation
type RegisterFormValues =
Omit<
UserProfile,
'id' | 'createdAt' | 'updatedAt' | 'role'
> & {
passwordConfirmation: string;
termsAccepted: boolean;
};
Später ändert sich die Entität:
interface UserProfile {
// ...
phoneNumber: string; // now required!
}
Die abgeleitete Formart erbt stillschweigend ein erforderliches phoneNumber, das der Registrierungsprozess nie gewollt hat. Es folgen weitere Anpassungen:
type RegisterFormValues =
Omit<
UserProfile,
'id' |
'createdAt' |
'updatedAt' |
'role' |
'phoneNumber'
> & {
phoneNumber?: string;
passwordConfirmation: string;
termsAccepted: boolean;
};
Jede Auslassung verstärkt die Unklarheit. Die Form und die Datenbankentität ändern sich aus unterschiedlichen Gründen; ihr Verknüpfen führt zu unerwarteten Fehlern.
Duplikation ist günstiger als die falsche Abstraktion
Schreiben Sie die Verträge getrennt auf:
// Database Entity Contract
export interface UserProfile {
id: string;
email: string;
fullName: string;
phoneNumber: string;
createdAt: string;
}
// Registration Form Contract
export interface RegisterFormValues {
email: string;
fullName: string;
phoneNumber?: string;
password: string;
passwordConfirmation: string;
termsAccepted: boolean;
}
Eine Handvoll duplizierter Felder kosten weniger als ein brüchiges Ableitungsgraphen. Falls zwei Typen aus unterschiedlichen Gründen ändern, sollten sie vermutlich nicht miteinander verbunden werden.
Falls zwei Typen aus unterschiedlichen Gründen ändern, sollten sie vermutlich nicht miteinander verbunden werden.
Eine nützliche Duftprüfung: Würde ein Produktmanager diese als dasselbe Konzept beschreiben? Eine Zeile mit Benutzerprofil in der Speicherung und ein Registrierungsformular auf einer Marketingseite teilen selten denselben Lebenszyklus, Überprüfungsregeln oder Besitzer. Wenn sich diese voneinander entfernen, verstärken abgeleitete Typen diesen Abstand zu Kompilierfehlern, die weit von der ursprünglichen Änderung entfernt sind. Explizite Schnittstellen machen diesen Abstand sichtbar und begrenzt. Mapping-Hilfsfunktionen – kleine Funktionen, die Werte von Entitäten in Formularstandardwerte umwandeln – gewährleisten eine ehrliche Konvertierung zur Laufzeit, ohne die Typidentitäten dauerhaft miteinander zu verbinden.
3. Diskriminierte Unionen sind besser als optionale Eigenschaften im Überfluss
Der Zustand einer asynchronen Benutzeroberfläche sieht oft so aus:
// The "Optional Flag" Anti-Pattern
interface RequestState<T> {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
data?: T;
error?: Error;
}
Die Nutzer erfinden anschließend illegale Kombinationen – isSuccess zusammen mit fehlenden data-Werten oder isLoading bei noch aktivierter Fehlermeldung:
if (state.isSuccess && state.data) {
return <div>{state.data.name}</div>;
}
Optionale Flags kodieren keine Zustandsmaschine; sie kodieren Hoffnung.
Die Macht diskriminierter Unionen
Machen Sie den Status explizit:
export type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
Die Darstellung wird ausführlich und sicher:
function RenderProfile({
state
}: {
state: AsyncState<UserProfile>;
}) {
switch (state.status) {
case 'idle':
return <div>Ready to load profile.</div>;
case 'loading':
return <LoadingSpinner />; case 'error':
return <ErrorMessage error={state.error} />; case 'success':
// TypeScript guarantees state.data exists here!
return <h1>Welcome, {state.data.fullName}</h1>;
}
}
Unmögliche Zustände verschwinden aus dem Typ, wodurch viele defensive Überprüfungen in der Benutzeroberfläche entfallen.
Unmögliche Zustände verschwinden aus dem Typ, wodurch viele defensive Überprüfungen in der Benutzeroberfläche entfallen.
Modelle mit optionalen Flaggen verursachen außerdem Verwirrung bei der Analyse und Protokollierung. Ist eine Anfrage erfolgreich, wenn isSuccess wahr ist, aber data undefiniert ist? Diskriminierte Unionen zwingen dazu, diese Frage beim Erstellen des Zustands zu beantworten – und nicht, wenn ein Junior-Entwickler im JSX ratet. Reducer sowie asynchrone Wrapper werden ebenfalls klarer: Jeder Übergang gibt eine vollständige Variante zurück, anstatt boolesche Werte zu wechseln, die sich widersprechen können.
4. Der Trugschluss von any vs unknown
Durch die Verwendung von any, um den Compiler zum Schweigen zu bringen, geht der Vorteil von TypeScript an dieser Stelle verloren. Verwenden Sie lieber unknown und schränken Sie den Typ mit Guard-Funktionen ein, wenn Sie API-Daten oder Inhalt aus localStorage lesen.
Ersetzen Sie any durch unknown + Typ- Guards
// Safe parsing of unknown API or localStorage data
function parseStoredPreferences(
raw: unknown
): UserPreferences {
if (
typeof raw === 'object' &&
raw !== null &&
'theme' in raw &&
(raw.theme === 'light' || raw.theme === 'dark')
) {
return {
theme: raw.theme,
fontSize:
typeof (raw as any).fontSize === 'number'
? (raw as any).fontSize
: 14,
};
}
// Safe fallback default
return {
theme: 'dark',
fontSize: 14
};
}
Außergewöhnliche Daten bleiben bis zur Überprüfung unzuverlässig; der interne Code erhält anschließend eine konkrete Struktur.
Außergewöhnliche Daten bleiben bis zur Überprüfung unzuverlässig; der interne Code erhält anschließend eine konkrete Struktur.
any ist an Modulgrenzen besonders schädlich, da es die nachfolgende Verarbeitung verfälscht: Ein einziger any aus JSON.parse kann alle Überprüfungen in einer ganzen Funktion außer Kraft setzen. unknown hingegen verhindert, dass solche Schäden entstehen. Verwenden Sie dazu Bibliotheken zur Schemaverifikation, wenn die übermittelten Daten groß sind, oder eigene, präzise geschriebene Überprüfungen, wenn die Strukturen klein und stabil sind. In jedem Fall sollte der Kern des Systems nur validierte Typen sehen.
5. Messung der Zeit für Typüberprüfungen in CI
Falls der Editor langsam wirkt, messen Sie zunächst, bevor Sie die Sprache dafür verantwortlich machen:
npx tsc --noEmit --extendedDiagnostics
Überwachen Sie die Anzahl der Instanziierungen, die Dateien sowie die Laufzeit. Teure rekursive Bedingungen dominieren oft. Falls das Typsystem eine hohe Kompilierkosten verursacht, muss es diese Kosten rechtfertigen.
Falls das Typsystem eine hohe Kompilierkosten verursacht, muss es diese Kosten rechtfertigen.
Erweiterte Diagnosewerkzeuge zeigen oft, dass nur wenige Dateien die meisten Instanziierungen verursachen – meist sind es zur Bequemlichkeit importierte rekursive Bedingungsfunktionen. Das Löschen oder Vereinfachen dieser Dateien kann bei jedem CI-Lauf Minuten einsparen. Verfolgen Sie die Zahlen im Laufe der Zeit genauso wie die Größe der Pakete. Eine Typarchitektur, die ihre Kompilierkosten nicht erklären kann, wird letztendlich dafür verantwortlich gemacht, dass „TypeScript langsam ist“, und das Team wird zu wenig in die Sprache investieren, obwohl sie dennoch zur Laufzeit Schutz bietet.
6. Das Problem mit typbezogener Cleverness
Klugheit ist eine weitere Falle: Typen, die ganze API-Oberflächen aus anderen Typen ableiten und anschließend immer mehr Bedingungen, mappierte Typen sowie Rekursion hinzufügen, bis niemand sie mehr anfasst. Fähigkeiten sind kein Grund, die aufwändigste Funktion zu verwenden.
Vergleichen Sie einen direkten indizierten Zugriff:
type UserName = UserProfile['fullName'];
mit einer rekursiven Hilfsfunktion, die jede Eigenschaft mit Zeichenkettenwert in einem beliebigen Objekt durchsucht. Wenn das Produkt nur UserProfile['fullName'] benötigt, erhöht die Komplexität das Risiko. Gute Ingenieursarbeit wählt das kleinste, verständliche Werkzeug. Ein Typ sollte sich nach sechs Monaten von selbst erklären lassen; „Berühren Sie das nicht“ bedeutet, dass die Abstraktion bereits versagt hat.
7. Ein pragmatisches TypeScript-Manifest
1. Schreiben Sie zunächst Typen für Menschen, erst danach für den Compiler
Falls Teamkollegen eine Definition nicht innerhalb einer Minute verstehen können, vereinfachen Sie sie. Eleganz, die nur der Autor versteht, ist Schulden.
2. Ziehen Sie Duplikation vor vorzeitiger Kopplung
Verzerrten Sie keine Komponenten-Schnittstelle nur deshalb, um drei Felder aus einer nicht verwandten Entität erneut zu verwenden. Trennen Sie Verantwortlichkeiten und Typen voneinander.
3. Verwenden Sie diskriminierte Unionen für den Zustand
Kodieren Sie echte Zustandsmaschinen mit einem Status-Diskriminator, damit der Compiler unmögliche Ausführungspfade entfernt.
4. Lassen Sie Generics niemals mehr als zwei Parameter haben
Drei oder mehr Generics deuten in der Regel darauf hin, dass die Abstraktion zu weit gefasst ist. Teilen Sie sie auf. Dies ist eine Heuristik, keine Regel – wenn Beziehungen schwer zu erklären sind, prüfen Sie das Design erneut.
5. Behandeln Sie externe Daten als unbekannt
API-Antworten, Speicherinhalte, Payloads von Drittanbietern sowie Benutzereingaben sollten an der Grenze überprüft werden, bevor sie zu vertrauenswürdigen Domänenobjekten werden.
6. Messen Sie erst, bevor Sie TypeScript die Schuld geben
Eine langsame CI-Prozessierung oder verzögerte Sprachdienste spiegeln in der Regel die Typarchitektur wider, nicht die Marke der Sprache. Erstellen Sie ein Profil und vereinfachen Sie anschließend die häufig genutzten Typen.
TypeScript sollte den Code langweilig machen
TypeScript ist am besten, wenn es im Hintergrund bleibt: Autocomplete-Funktionen, sichere Refaktorisierungen und weniger Überraschungen während der Ausführung. Es sollte sich nicht wie ein Rätsel bei jeder Änderung eines Komponentenverhaltens anfühlen. Die weniger beeindruckenden Schnittstellen – einfache Geschäftsobjekte – sind oft am wertvollsten. Halten Sie die Typen einfach, praktisch und in Beziehung zu den tatsächlichen Produktkonzepten, damit das Team Produkte veröffentlichen kann, anstatt sich mit Typalgebra zu beschäftigen.
Halten Sie die Typen einfach, praktisch und in Beziehung zu den tatsächlichen Produktkonzepten, damit das Team Produkte veröffentlichen kann, anstatt sich mit Typalgebra zu beschäftigen.
Retrospektiven verbessern sich, wenn das Gespräch vom Sprachkonzept auf die Gestaltungsoptionen verlagert wird: Wie viele Generika, wie viel Ableitung, wie ehrlich ist die Zustandsmaschine, wie gelangen externe Daten in die App. TypeScript belohnt diese Ehrlichkeit mit schnelleren Rückmeldungen zu den wichtigen Änderungen. Cleverness bestraft es hingegen mit undurchsichtigen Fehlern. Wählen Sie absichtlich den langweiligeren Weg, dokumentieren Sie die wenigen fortgeschrittenen Typen, die tatsächlich entscheidend sind, und lassen Sie rätselhafte Muster aus den gemeinsamen Bibliotheken heraus, mit denen Ihre neuen Entwickler bereits am ersten Tag arbeiten müssen.
Wenn eine Änderung durch Typen blockiert erscheint, fragen Sie sich, ob das Modell das Produkt widerspiegelt. Oft liegt die Lösung nicht in komplexeren Bedingungen, sondern in einer klareren Schnittstelle, einem geteilten Modul oder einer Union, die die Zustände benennt, über die Sie bereits im Stand-up sprechen. So hört TypeScript auf, eine Last zu sein, und wird wieder zu einem Vorteil.