Startseite / Artikel / TypScript-Konventionen, die den Code streng und flexibel halten

TypScript-Konventionen, die den Code streng und flexibel halten

Praktische Ansichten zu TypeScript: Strict Mode, unknown über any, spärliche Assertionen, Zeichenkettenunionen, Typ versus Interface, JSDoc-Properties, Hilfsfunktionen und diskriminierte Unionen.

1535 Wörter

Guter TypeScript sollte die Wahrscheinlichkeit verringern, dass fehlerhafter Code veröffentlicht wird, ohne jede Datei in einen bürokratischen Prozess zu verwandeln. Die unten aufgeführten Konventionen fördern einfachere, sicherere und wartbarere Strukturen, während sie die Flexibilität beibehalten, die TypeScript im Alltag nützlich macht.

Kurzfassung

  • Definieren Sie Verträge an den Rändern von Modulen und Komponenten ausführlich.
  • Erlauben Sie dem Prüfer, private Hilfsfunktionen und lokale Variablen abzuleiten.
  • Halten Sie Einschränkungen klein und beabsichtigt.
  • Nehmen Sie vor dem Erfinden neuer Aliase und Hilfsfunktionen bereits vorhandene zu Hilfe.
  • Fördern Sie bei alternativen Zuständen literale Unionen und markierte Varianten.

Strenge Modus aktivieren

Aktivieren Sie immer die strenge Prüfung in tsconfig.json (Handbuch):

{
  "compilerOptions": {
    "strict": true
  }
}

Die strengen Optionen erkennen häufige Fehler früher und gewährleisten einen höheren Standard an Korrektheit in einer wachsenden Codebasis.

Vermeiden Sie den Typ any

Falls der Typ eines Wertes ungewiss ist, sollte man unknown statt any verwenden, da in diesem Fall zunächst eine Einengung des Typs erforderlich ist:

// bad
function processValue(value: any) {
  value.toLowerCase(); // No error, but might crash at runtime
}

// good
function processValue(value: unknown) {
  if (typeof value === 'string') {
    value.toLowerCase(); // OK, we've verified it’s a string
  }
}

Typüberprüfungen ignorieren

Falls tatsächlich ein Umgehen notwendig ist, sollte die Notlösung so klein wie möglich gehalten werden.

Das Umwandeln einer einzelnen Ausdrucksform mit as any ist oft besser als @ts-ignore, denn die Umwandlung betrifft nur diesen Ausdruck, während @ts-ignore ganze nachfolgende Zeilen ignorieren kann.

// third-party library with incomplete type definitions
import { someExternalLib } from 'external-lib';

const result = (someExternalLib.complexMethod() as any).undocumentedProperty;

In eigenem Code sollte man stattdessen versuchen, die Typen zu korrigieren, Schutzmechanismen hinzuzufügen oder externe Daten zu validieren (zum Beispiel mit Zod), anstatt die Typüberprüfung einfach auszuschalten.

Nutzen Sie @ts-expect-error, wenn Sie mehr wissen als die Typüberprüfung nachweisen kann:

  • Im Gegensatz zu @ts-ignore scheitert die Kompilierung, wenn der Fehler unerwartet verschwindet.
  • Dadurch bleiben absichtliche Unterdrückungen bei der Überprüfung ehrlich dargestellt.
<div
  style={{
    // @ts-expect-error — vars are valid CSS properties
    '--size': `${dimensions.size}px`,
  }}
/>

Verwenden Sie Typbehauptungen sparsam

as-Behauptungen überschreiben den Überprüfer. Ziehen Sie bei Möglichkeit eine Überprüfung zur Laufzeit vor:

// bad
const myCanvas = document.getElementById('canvas') as HTMLCanvasElement;

// good
const myCanvas = document.getElementById('canvas');
if (myCanvas instanceof HTMLCanvasElement) {
  const ctx = myCanvas.getContext('2d');
}

Ziehen Sie Typschutzmechanismen vor Behauptungen vor

Schutzmechanismen beschränken den Typ erst nach erfolgreichem Laufzeitcheck – im Vergleich zu einer einfachen as-Behauptung ist dies fehlersicherer:

type User = {
  id: string;
  name: string;
};

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof value.id === 'string' &&
    'name' in value &&
    typeof value.name === 'string'
  );
}

function greet(value: unknown) {
  if (isUser(value)) {
    // value is User here - no assertion needed
    console.log(`Hello, ${value.name}`);
  }
}

Verwenden Sie eine Behauptung nur dann, wenn eine Laufzeitüberprüfung unmöglich oder sinnlos ist – typischerweise, wenn Eigenschaften von Drittanbietern nicht zum umgebenden Formmuster passen:

<OTPInput
  // Our form pattern expects a traditional change handler but this third-party
  // component provides the text value as its only callback argument. We need
  // to manually create an event-like object so validation works as expected
  // for consumers.
  onChange={(value) => {
    props.onChange?.({
      target: { value },
    } as ChangeEvent<HTMLInputElement>);
  }}
/>

Ziehen Sie Unionen von Zeichenkettenliteralen vor Enums

Für Anwendungscode sind Unionen von Zeichenkettenliteralen in der Regel besser als Enums: kein Laufzeitobjekt, einfacherer Ausgabeablauf und bessere Unterstützung bei der Serialisierung:

// bad
enum Environments {
  DEV = 'dev',
  TESTING = 'test',
  STAGING = 'staging',
  PRODUCTION = 'prod',
}

// good
type Environments = 'dev' | 'test' | 'staging' | 'prod';

Bewahren Sie Enums bei, wenn ein Laufzeitobjekt erforderlich ist, eine bestehende API bereits Enums verwendet oder ein Teamstandard ihre Verwendung vorschreibt.

Ermitteln Sie Unionen aus Werten

Falls Werte bereits zur Laufzeit vorhanden sind, ergeben Sie die Vereinigung aus einer einzigen Quelle der Wahrheit:

const ENVIRONMENT_META = {
  dev: {},
  test: {},
  staging: {},
  prod: {},
};

type Environments = keyof typeof ENVIRONMENT_META;
//   ^? 'dev' | 'test' | 'staging' | 'prod'

Array-Form:

const ENVIRONMENTS = ['dev', 'test', 'staging', 'prod'] as const;
type Environments = (typeof ENVIRONMENTS)[number];
//   ^? 'dev' | 'test' | 'staging' | 'prod'

Eine const-Aussage ist erforderlich, damit das Array weiterhin eine Tupel aus Literalen bleibt und nicht zu string[] wird.

Verwenden Sie type statt interface

Für den größten Teil des Anwendungscode deckt type Objektstrukturen sowie Vereinigungen, Schnittmengen, mappierte und bedingte Typen ab. Zudem kann er nicht versehentlich wieder geöffnet werden:

// when required
interface Person {
  name: string;
  email: string;
}

// preferred
type Person = {
  name: string;
  email: string;
};

Durch die Verwendung einer einzigen Form wird ein unnötiges Wechseln zwischen überschneidenden Funktionen vermieden.

Verwenden Sie ein Interface nur dann, wenn seine Eigenschaften wichtig sind

Publikche Bibliotheken können absichtlich interface exportieren, damit Nutzer Erweiterungen vornehmen können:

// library
export interface Theme {
  colors: Record<string, string>;
}

// consumer: extend the interface without modifying the original source
declare module 'my-library' {
  interface Theme {
    spacing: Record<string, number>;
  }
}

Dokumentieren Sie die Eigenschaften von Komponenten

Verwenden Sie JSDoc (/** ... */) für öffentliche oder nicht offensichtliche Eigenschaften – Zweck, Verhalten, Einschränkungen – solange der Kontext noch frisch ist:

type SearchFieldProps = {
  /**
   * A unique identifier for the field. Consider [useId](https://react.dev/reference/react/useId) to generate a unique ID.
   */
  id?: string;
  /**
   * The label of the field. For "visually hidden" labels, use the `aria-label`
   * attribute.
   */
  label?: string;
  /**
   * @deprecated Use `label` instead.
   */
  title?: string;
  /**
   * Handler that is called when the field is submitted.
   */
  onSubmit?: (value: string) => void;
  /**
   * Handler that is called when the clear button, or <Escape> key, is pressed.
   */
  onClear?: () => void;
} & AriaLabellingProps;

Ausnahmen bei der Dokumentation

Vermeiden Sie umfangreiche Dokumentationen, wenn:

  • Die Komponente nur einmal verwendet wird und voraussichtlich nicht erneut aufgerufen wird
  • Es sich um ein privates Kind einer dokumentierten Elternkomponente handelt, deren Vertragsbedingungen das Verhalten bereits erklären

Sollten Sie unsicher sein, wählen Sie Klarheit und bleiben Sie konsistent.

Wiederverwenden vorhandener Typen

Machen Sie öffentliche APIs explizit; überlassen Sie die Interna der Inferenz. Bevor Sie eine neue Abstraktion entwickeln, suchen Sie nach einem vorhandenen Typ, einer Eigenschaft oder einer Hilfsmethode, die wiederverwendet werden kann, damit verwandter Code zusammenhängt.

Abschluss auf Eigenschaften

Durch indizierten Zugriff wird der Typ einer Eigenschaft aus einem vorhandenen Alias abgerufen:

import type { ThingProps } from '../thing';

type SpecialThingProps = {
  ...
  label?: ThingProps['label'];
  type?: ThingProps['type'];
};

function SpecialThing(props: SpecialThingProps) {}

Zur Kombination von Typen: Intersektionen bevorzugen

Kombinieren Sie Typen, ohne Felder (und deren Dokumentation) zu duplizieren:

type Person = { name: string; email: string };
type User = Person & { id: string };
//   ^? { name: string; email: string; id: string }
import type { ThingProps } from '../thing';

type SpecialThingProps = {
  ...
} & Pick<ThingProps, 'label' | 'type'>;

function SpecialThing(props: SpecialThingProps) {}

Detaillierter Implementierungsaufschluss

Die Inferenz hält die internen Typen in Einklang mit den Werten:

function initContext() {
  return {
    name: 'Jane Doe',
    email: 'jane.doe@example.com',
  };
}

type Context = ReturnType<typeof initContext>;
//   ^? { name: string; email: string }

ComponentProps leitet Eigenschaften aus einer React-Komponente ab, wenn die Bibliothek diese nicht exportiert:

import type { ComponentProps } from 'react';
import { RouterProvider } from 'react-aria-components';

type RouterProviderProps = ComponentProps<typeof RouterProvider>;
//  {
//    navigate: (path: Href, routerOptions: RouterOptions | undefined) => void;
//    useHref?: (href: Href) => string;
//    children: ReactNode;
//  }

Parameters liefert ein Argument-Tupel, das Sie indizieren können:

// library code

declare function someLibFn(
  value: number,
  options: {
    minimumFractionDigits: number;
    maximumFractionDigits: number;
  }
): void;

// consumer code

import { someLibFn } from 'some-lib';

type LibFnValue = Parameters<typeof someLibFn>[0];
//   ^? number
type LibFnOptions = Parameters<typeof someLibFn>[1];
//   ^? { minimumFractionDigits: number, maximumFractionDigits: number }

Nutzen Sie eingebaute Hilfstypen

Hilfstypen umfassen gängige Transformationen und sind für neue Teammitglieder leicht verständlich:

type User = {
  id: string;
  name: string;
  email: string;
};

type UserPatch = Partial<User>;
type UserDetails = Omit<User, 'id'>;
type UserIdentity = Pick<User, 'id' | 'email'>;

Schreiben Sie nur dann einen benutzerdefinierten Hilfstyp, wenn der Name der Transformation ein echtes Fachkonzept beschreibt – und nicht nur, um Tastenanschläge zu sparen.

Nutzen Sie diskriminierte Unionen

Getaggte Unionen modellieren Alternativen, sodass eine Vollständigkeitsprüfung funktioniert:

type Actions =
  | { type: 'login'; username: string }
  | { type: 'logout'; reason: 'session-timeout' | undefined }
  | { type: 'update'; id: string; data: Record<string, unknown> }

Ein gemeinsames Literalfeld (type oder kind) ermöglicht es TypeScript, jeden Zweig sicher einzugrenzen.

Zusammenfassung

Konstanz ist wichtiger als jede einzelne Regel. Einigen Sie sich auf Konventionen, dokumentieren Sie Ausnahmen und optimieren Sie für den nächsten Leser:

  • Klare Randfälle
  • Aus den internen Strukturen abgeleitete Regeln
  • Minimale Unterdrückungen
  • Gemeinsame Pseudonyme und Hilfsfunktionen
  • Markierte Unionen, bei denen illegale Kombinationen verschwinden müssen

Wenn diese Gewohnheiten gemeinsam angewandt werden, bleibt TypeScript ein Entwurfswerkzeug und kein Belastungsfaktor: Öffentliche APIs bleiben eindeutig, die internen Strukturen bleiben DRY, und die verbleibenden any- oder as-Umwandlungen werden selten und sind als überprüfbare Ausnahmen zu betrachten, statt als Standardausweg.

Diese Referenzen finden sich im offiziellen TypeScript-Leitfaden sowie in den JSDoc-Dokumentationen für Leser, die die kanonischen Formulierungen benötigen.

Betrachten Sie die Liste eher als ein lebendiges Teamabkommen denn als eine für immer gültige Dogmatik. Einverstanden.