Type Annotations sind die explizite Form, dem TypeScript-Compiler einen Typ mitzuteilen — gesetzt nach einem Doppelpunkt, direkt hinter dem Bezeichner. Sie stehen im Gegensatz zur Inferenz, bei der TypeScript den Typ aus dem Kontext ableitet. Beides hat seine Berechtigung: Annotations machen Verträge sichtbar und stabilisieren Public APIs, Inferenz hält den Code kurz und idiomatisch. Dieser Artikel zeigt dir die Syntax für Variablen, Parameter, Return-Typen, Properties, Function-Types und Generics — und gibt dir eine klare Heuristik, wann du explizit annotierst und wann du den Compiler arbeiten lässt.

Was Type Annotations sind

Eine Type Annotation ist eine explizite Typ-Angabe in der Quellcode-Syntax. Du schreibst sie nach einem Doppelpunkt direkt hinter den Bezeichner — sei es eine Variable, ein Parameter, eine Property oder ein Return-Wert.

ts annotation-grundform.ts
// Form: <bezeichner>: <typ>
let alter: number = 42;
let name: string = "Mira";
let aktiv: boolean = true;

Die Annotation steht rechts vom Bezeichner, nicht davor — TypeScript folgt damit der Konvention moderner getypter Sprachen wie Rust, Go oder Swift, nicht der von Java oder C#. Die Annotation wird beim Transpilieren entfernt; zur Laufzeit existieren nur die nackten JavaScript-Werte.

Annotations sind ein Vertrag mit dem Compiler: du sagst „dieser Wert muss von diesem Typ sein". Weicht eine Zuweisung davon ab, meldet tsc den Verstoß zur Übersetzungszeit — nicht erst, wenn der Code läuft.

Variablen-Annotations

Bei let, const und var steht die Annotation zwischen Bezeichner und Initialwert:

ts variablen.ts
let zähler: number = 0;
const titel: string = "TypeScript";
let werte: number[] = [1, 2, 3];
let nutzer: { id: number; name: string } = { id: 1, name: "A" };

Du kannst die Annotation auch ohne Initialwert setzen — dann ist die Variable bis zur ersten Zuweisung undefined, der Compiler weiß aber bereits, welcher Typ erwartet wird:

ts deklaration-ohne-init.ts
let antwort: number;
// antwort = "42";   // Fehler: Type 'string' is not assignable to type 'number'.
antwort = 42;        // OK

Bei einem Initialwert ist die Annotation in den meisten Fällen redundant, weil TypeScript den Typ aus der rechten Seite inferiert. let zähler = 0 und let zähler: number = 0 sind in dieser Hinsicht äquivalent. Die Annotation wird erst dann interessant, wenn der Initialwert mehrdeutig ist — zum Beispiel null, ein leeres Array oder ein generischer Wert.

ts wann-annotation.ts
// Ohne Annotation: TypeScript inferiert 'any[]' (mit noImplicitAny: never[])
const ids = [];

// Mit Annotation: klarer Vertrag, hilft beim push
const idsTyped: number[] = [];
idsTyped.push(1);
// idsTyped.push("x");   // Fehler: 'string' is not assignable to 'number'.

Parameter-Annotations

Funktions-Parameter werden fast immer annotiert. Ohne Annotation fallen sie auf den Typ any zurück — was bei aktivem noImplicitAny ein Compiler-Fehler ist.

ts parameter.ts
function begrüßen(name: string, alter: number): string {
    return `Hallo ${name}, du bist ${alter} Jahre alt.`;
}

// Ohne Annotation: implicit any (mit noImplicitAny ein Fehler)
function lose(x) {        // Parameter 'x' implicitly has an 'any' type.
    return x * 2;
}

Optionale Parameter markierst du mit einem Fragezeichen hinter dem Namen. Der resultierende Typ ist T | undefined:

ts optional-parameter.ts
function grüßen(name: string, titel?: string): string {
    if (titel) return `Hallo ${titel} ${name}`;
    return `Hallo ${name}`;
}

grüßen("Mira");           // OK
grüßen("Mira", "Frau");   // OK
// grüßen("Mira", undefined);  // ebenfalls OK

Mit einem Default-Wert wird ein Parameter implizit optional — du brauchst kein zusätzliches Fragezeichen, und der Typ entspricht dem nicht-optionalen Basistyp:

ts default-parameter.ts
function inkrement(wert: number, schritt: number = 1): number {
    return wert + schritt;
}

inkrement(10);      // 11
inkrement(10, 5);   // 15

Rest-Parameter werden als Array annotiert:

ts rest-parameter.ts
function summe(...zahlen: number[]): number {
    return zahlen.reduce((a, b) => a + b, 0);
}

summe(1, 2, 3, 4);   // 10

Return-Type-Annotations

Der Return-Typ steht hinter der Parameter-Liste, getrennt durch einen Doppelpunkt:

ts return-typ.ts
function addieren(a: number, b: number): number {
    return a + b;
}

function logge(meldung: string): void {
    console.log(meldung);
}

// Funktion, die nie zurückkehrt:
function fehler(msg: string): never {
    throw new Error(msg);
}

TypeScript kann den Return-Typ fast immer inferieren. Trotzdem ist eine explizite Annotation bei Funktionen, die andere Module konsumieren, eine gute Praxis:

  • Sie macht die Signatur selbstdokumentierend — Leser sehen den Vertrag, ohne den Funktionskörper lesen zu müssen.
  • Sie schützt vor unbeabsichtigten Änderungen: ein Refactoring, das versehentlich einen anderen Typ zurückgibt, fällt sofort auf — statt sich erst beim Aufrufer als Folgefehler zu zeigen.
  • Sie macht Type-Errors lokal: ohne Annotation propagiert ein falscher Return durch alle Aufrufe; mit Annotation explodiert es an der Quelle.
ts warum-explizit.ts
// Ohne Annotation: Compiler inferiert versehentlich 'string | number'
function preisFormatieren(wert: number) {
    if (wert < 0) return "ungültig";
    return wert;
}

// Mit Annotation: Fehler bleibt LOKAL
function preisFormatierenStrikt(wert: number): string {
    if (wert < 0) return "ungültig";
    return wert;   // Fehler: 'number' is not assignable to 'string'.
}

Faustregel: interne Helfer-Funktionen ohne Annotation, exportierte Funktionen und Modul-Grenzen mit Annotation.

Property-Annotations in Objekten

In Object-Types, Interfaces und Klassen werden Properties wie Variablen annotiert. Das Fragezeichen markiert auch hier optionale Properties.

ts object-properties.ts
// Inline Object-Type
const punkt: { x: number; y: number; z?: number } = { x: 1, y: 2 };

// Als type-Alias
type Punkt = {
    x: number;
    y: number;
    z?: number;
};

// Als interface
interface Nutzer {
    id: number;
    name: string;
    email?: string;          // optional
    readonly erstelltAm: Date;
}

Das Fragezeichen ist semantisch identisch mit | undefined im Property-Typ — mit einem Unterschied: ein optionales Property darf beim Erzeugen des Objekts ganz weggelassen werden, eine explizite Union mit undefined muss vorhanden, aber undefined sein.

ts optional-vs-undefined.ts
type A = { x: number; y?: number };
type B = { x: number; y: number | undefined };

const a: A = { x: 1 };                  // OK — y darf fehlen
const b: B = { x: 1 };                  // Fehler: Property 'y' is missing.
const b2: B = { x: 1, y: undefined };   // OK

Function-Type-Annotations

Variablen, Parameter und Properties, die Funktionen halten, brauchen einen Function-Type. Die Syntax dafür ist die Arrow-Function-Notation auf Type-Ebene:

ts function-type.ts
// (param1: T1, param2: T2) => ReturnType
let cb: (x: number) => void;

cb = (n) => console.log(n);   // Parameter 'n' wird als number inferiert
cb(42);

// Als Parameter-Typ:
function fürJedes(arr: number[], fn: (wert: number, index: number) => void): void {
    arr.forEach(fn);
}

Wichtig: die Parameter-Namen in der Annotation sind nicht optional(string) => void heißt nicht „Funktion mit string-Parameter", sondern „Funktion mit einem Parameter namens string vom Typ any". Immer Name UND Typ schreiben.

Für komplexere Fälle gibt es die Call-Signature in einem Object-Type — praktisch, wenn die Funktion zusätzlich Properties trägt:

ts call-signature.ts
type Logger = {
    level: "info" | "warn" | "error";
    (meldung: string): void;
};

function nutze(log: Logger) {
    log("Start");
    console.log(log.level);
}

Wird die Funktion mit new aufgerufen, brauchst du eine Construct-Signature:

ts construct-signature.ts
type DateFactory = {
    new (iso: string): Date;
};

const f: DateFactory = Date;
const d = new f("2026-01-01");

Object-Method-Annotations vs. Function-Property-Annotations

In Object-Types kannst du eine Funktion auf zwei Arten annotieren — als Method-Shorthand oder als Property mit Function-Type. Semantisch fast gleich, aber mit einem wichtigen Unterschied beim Strict-Checking.

ts method-vs-property.ts
// Method-Shorthand:
interface RepoMethod {
    speichern(item: string): void;
}

// Function-Property (Arrow-Style):
interface RepoProperty {
    speichern: (item: string) => void;
}

Bei aktiver strictFunctionTypes-Option werden Function-Properties bivariant strenger geprüft als Methoden. Methoden bleiben aus historischen Gründen bivariant — das ist meist „gut genug", aber wer maximale Soundness will, nutzt die Property-Form.

In der Praxis: Methode, wenn die Funktion fest zum Objekt gehört. Function-Property, wenn die Funktion ein austauschbarer Callback ist.

Generic-Parameter-Annotations

Funktionen können generisch sein — sie binden einen Typ-Parameter, den der Aufrufer (oder die Inferenz) konkretisiert. Die Typ-Parameter stehen in spitzen Klammern <T> direkt vor der Parameter-Liste.

ts generic-funktion.ts
function identität<T>(wert: T): T {
    return wert;
}

const s = identität<string>("Hallo");   // explizit
const n = identität(42);                // inferiert -> T = number

// Mehrere Typ-Parameter:
function paar<A, B>(a: A, b: B): [A, B] {
    return [a, b];
}

Typ-Parameter können constrained werden mit extends:

ts generic-constraint.ts
function länge<T extends { length: number }>(x: T): number {
    return x.length;
}

länge("Hallo");        // OK, string hat .length
länge([1, 2, 3]);      // OK, array hat .length
// länge(42);          // Fehler: number hat keine .length-Property.

Generic-Annotations geben dir das Beste aus beiden Welten: maximale Wiederverwendbarkeit ohne any, mit voller Typ-Kontrolle am Aufrufpunkt.

Wann annotieren, wann inferieren lassen?

Eine pragmatische Heuristik, die sich in größeren Codebases bewährt:

StelleEmpfehlungBegründung
Lokale Variable mit InitialwertinferierenAnnotation wäre redundant — verbraucht Zeichen ohne Mehrwert.
Lokale Variable ohne InitialwertannotierenSonst implicit any.
Funktions-Parameterimmer annotierenSonst implicit any (oder noImplicitAny-Fehler).
Return-Typ interner HelperinferierenKompakter, Aenderungen propagieren automatisch.
Return-Typ exportierter FunktionannotierenStabilisiert Public-API, macht Fehler lokal.
Object-Literal als Konstanteinferieren (ggf. as const)TypeScript leitet präzise Typen ab.
Klassen-PropertiesannotierenVertrag der Klasse wird sichtbar.
Callback-Parameter in .map, .filter etc.inferierenTypeScript kennt den Array-Element-Typ.

Die Kernaussage des Handbooks: schreib weniger Annotations als du denkst — und nutze sie dort, wo sie Lesern und Maintainern den größten Nutzen bringen: an Modul-Grenzen, an exportierten Signaturen und überall dort, wo Inferenz mehrdeutig oder verwirrend wird.

FAQ

Muss ich jede Variable annotieren?

Nein. Bei lokalen Variablen mit Initialwert ist Inferenz fast immer ausreichend — und meist sogar besser, weil sie präzisere Typen ableitet als du sie schreiben würdest. Annotation lohnt sich nur, wenn der Initialwert mehrdeutig ist (leeres Array, null) oder du den Typ bewusst weiter fassen willst als die Inferenz.

Soll ich Funktions-Return immer annotieren?

Bei exportierten Funktionen und Public-API ja — sie sind der Vertrag mit deinen Konsumenten und der Compiler hält versehentliche Änderungen lokal. Bei kurzen internen Helfern kannst du inferieren lassen; Lesbarkeit und Refactoring-Komfort wiegen dort höher als formale Strenge.

Warum sollte ich Parameter immer annotieren?

Ohne Annotation fallen Parameter auf any zurück — der Compiler kann nicht aus dem Funktionskörper rückwärts inferieren, was der Aufrufer übergeben sollte. Mit aktivem noImplicitAny (Default in strict) ist das sogar ein Fehler. Parameter sind der einzige Ort, an dem Annotation fast immer Pflicht ist.

Was ist der Unterschied zwischen x: number und x as number?

Eine Annotation x: number ist ein Vertrag: der Compiler prüft, dass jede Zuweisung an x wirklich number ist. Eine Assertion x as number ist eine Behauptung: du sagst dem Compiler „vertrau mir, das ist eine Zahl", ohne dass er es verifizieren kann. Annotation ist sicher, Assertion ist eine Abkürzung mit Verantwortung.

Kann ich nur einige Parameter einer Funktion annotieren?

Nein, nicht selektiv. Entweder du gibst allen Parametern Typen oder keinem — wobei „keinem" bedeutet, dass alle implicit any sind (was mit noImplicitAny fällt). In manchen Kontexten kann TypeScript Parameter kontextuell inferieren, etwa bei Array-Callbacks: [1,2,3].map(n => n*2) — hier weiß der Compiler, dass n eine number ist, ohne Annotation.

Kann ich Type-Annotations in einer reinen JS-Datei nutzen?

Direkt nein — die : type-Syntax ist nur in .ts erlaubt. Aber: mit // @ts-check oben in der .js-Datei und JSDoc-Kommentaren erhältst du fast die volle Typprüfung. /** @param {number} x */ ist das JSDoc-Äquivalent zu (x: number). Praktisch für schrittweise Migration von Legacy-Codebases.

Was kostet eine fehlende Annotation in der Praxis?

Implicit any. Damit verlierst du den Schutz von TypeScript an dieser Stelle vollständig — jede Operation darauf ist erlaubt, jeder Zugriff ungeprüft. Aktiviere noImplicitAny: true (oder gleich strict: true) in der tsconfig.json, damit der Compiler dich auf solche Lücken hinweist, statt sie stillschweigend zu akzeptieren.

Wie mache ich mehrzeilige Function-Type-Annotations lesbar?

Sobald eine Function-Type-Annotation länger wird, lager sie in einen type-Alias aus: type Handler = (req: Request, res: Response) => Promise<void>;. Das macht den Aufruf-Code lesbar (const fn: Handler = ...), erlaubt Wiederverwendung und gibt der Funktionssignatur einen sprechenden Namen — kleine Investition, großer Lesbarkeits-Gewinn.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Type System

Zur Übersicht