Das TypeScript Type System bildet das Fundament für moderne JavaScript-Entwicklung mit Typsicherheit. Als statisches Typsystem ermöglicht es frühzeitige Fehlererkennung, verbesserte Code-Qualität und präzisere Entwicklerwerkzeuge. TypeScript erweitert JavaScript durch Typannotationen, Interfaces, Generics und fortgeschrittene Typoperationen, ohne die Flexibilität von JavaScript einzuschränken. Während der Kompilierung werden Typinformationen geprüft und anschließend zu standardkonformem JavaScript transformiert, was sowohl Entwicklungseffizienz als auch Laufzeitperformance optimiert. Diese Einführung beleuchtet die grundlegenden Konzepte des TypeScript Type Systems sowie dessen praktischen Nutzen für skalierbare Anwendungen.

Was ist ein Type System?

Ein Type System ist ein Regelwerk, das jedem Ausdruck in einem Programm einen “Typ” zuordnet und prüft, ob diese Typen konsistent verwendet werden.

Es beantwortet Fragen wie:

  • Welche Operationen sind auf einem Wert erlaubt
  • Welche Werte kann eine Variable enthalten
  • Welche Argumente erwartet eine Funktion

** Statisch vs dynamisch typisiert**

Bei JavaScript ist es erlaubt, die Typen zur Laufzeit zu ändern.

TypeScript JavaScript - Dynamisch
let value = 42; // value ist eine Zahl
value = "Hello"; // Jetzt ist value ein String
value = [1, 2, 3]; // Jetzt ist alue ein Array

In TypeScript kann man den festgelegten Typ nicht ändern.

TypeScript TypeScript - Statisch
let value: number = 42; // value muss eine Zahl sein
value = "Hello"; // Error: Type 'string' is not assignable to type 'number'

Type Annotations - Die Basis

Type Annotations sind explizite Typangaben, die dem Compiler mitteilen, welchen Typ eine Variable, Parameter oder Rückgabewerte haben soll. Die Syntax verwendet einen Doppelpunkt gefolgt vom Typ.

Die Syntax lautet wie folgt.

TypeScript
let variablenName: Type = value;
TypeScript Beispiel
let age: number = 25;
let name: string = "John";
let isActive: boolean = true;

function greet(name: string): string {
    return `Hello ${name}`;
}

Warum Type Annotations verwenden?

  • Explizite Absicht: Man dokumentiert, was der Code tun soll
  • Frühe Fehlererkennung: Typ-Fehler werden sofort erkannt
  • Bessere IDE-Unterstützung: Autocompletion und Refactoring
  • Arbeit im Team: Andere Entwickler verstehen besser die Erwartungen

Type Inference

Type Inference ist die Fähigkeit von TypeScript, Typen automatisch abzuleiten, ohne dass man sie explizit angeben muss. Dies macht TypeScript weniger verbose und angenehmer zu schreiben.

Funktion von Type Inference

Schauen wir uns ein Beispiel für Verwendung von Type Inference. TypeScript leitet den Typ aus dem Initialwert ab.

TypeScript Beispiel
let count = 0;                  // Typ: number
let message = "Hello";          // Typ: string
let isReady = false;            // Typ: boolean
let nums = [1, 2, 3];           // Typ: number[]
let mixed = [1, "two", true];   // Typ: (string | number | boolean)[]

function add(a: number, b: number) {
    return a + b; // Rückgabewert Typ: number
}

const user = {
    name: "John",
    age: 30,
    roles: ["admin", "user"]
};
// Typ: { name: string, age: number, roles: string[] }

Wann sollten Type Annotations verwendet werden?

Es kann mehrere Gründe geben, warum und wann man Type Annotations verwenden sollte.

  • Funktionsparameter: Immer annotieren für Klarheit
  • Öffentliche APIs: Explizite Typen für Schnittstellen
  • Wenn Inference nicht ausreicht: Bei komplexen Typen
  • Zur Dokumentation: Wenn der Typ nicht offensichtlich ist

Gute Regel

Ein guter Weg ist es, die Annotationen immer zu verwenden. So hat man die bestmögliche Qualität und muss nicht nachdenken, ob eine Annotation an bestimmter Stelle sinnig oder sogar notwendig ist.

TypeScript
// Inference reicht aus
let simpleText = "Hello";

// Annotation sinnvoll für Klarheit
let id: string | number = getUserId(); // Macht klar, dass beide Typen möglich sind

// Annotation notwendig
let values: number[] = []; // Ohne Annotation wäre es 'any[]'

Strukturelle Typisierung

TypeScript verwendet strukturelle Typisierung (auch Duck Typing genannt). Das bedeutet, dass die Kompatibiltät von Typen auf ihrer Struktur basiert, nicht auf ihrer Deklaration.

Prinzip: Wenn es wie eine Ente aussieht und wie eine Ente quakt, dann ist es eine Ente.

TypeScript Beispiel
interface Point {
    x: number;
    y: number;
}

interface Coordinate {
    x: number;
    y: number;
}

// Diese Typen sind kompatibel,
// obwohl sie unterschiedliche Namen haben
let point: Point = { x: 10, y: 20 };
let coord: Coordinate = point; // Ok - gleiche Struktur

// Auch ohne explizite Interface-Implementierung
class Position {
    constructor(public x: number, public y: number) {}
}

// Ok - Position hat x und y
let pos: Point = new Position(5, 10);

Vorteile der strukturellen Typisierung

  • Flexibilität: Code funktioniert mit allen kompatiblen Strukturen
  • Keine explizite Implementierung nötig: Interfaces müssen nicht explizit implementiert werden
  • JavaScript-Kompatibiltät: Passt zu JavaScript dynamischer Natur

Excess Property Checks

Bei Objekt-Literalen führt TypeScript zusätzliche Prüfungen durch.

TypeScript Beispiel
interface Person {
    name: string;
    age: number;
}

// Direkte Zuweisung - Excess Property Check
let person: Person = {
    name: "John",
    age: 30,
    job: "Developer" // Error: 'job' does not exist in type 'Person'
};

// Über Variable - kein Excess Property Check
let someObject = { name: "John", age: 25, job: "Designer" };
let person2: Person = someObject; // Ok - hat mindestens 'name' und 'age'

Type Compatibility

TypeScript prüft Typ-Kompatibiltät basierend auf der Struktur. Folgende Regeln gelten hierbei.

1. Objekt-Kompatibiltät

TypeScript
interface Animal {
    name: string;
}

interface Dog {
    name: string;
    breed: string;
}

let animal: Animal;
let dog: Dog = { name: "Buddy", breed: "Golden Retriever" };

// Ok - Dog hat alle Properties von Animal
animal = dog;

// Fehler - Bei Animal fehlt 'breed'
// dog = animal;

2. Funktions-Kompatibiltät

TypeScript
type Handler = (x: number) => void;

// Weniger Parameter sind OK
let h1: Handler = (x: number) => console.log(x); // Passt
let h2: Handler = () => console.log("no params"); // Passt
let h3: Handler = (x: number, y: number) => console.log(x, y); // Error

// Return-Typ muss kompatibel sein
type NumberFunc = () => number;
type StringFunc = () => string;

let nf: NumberFunc = () => 42;
let sf: StringFunc = () => "Hello";

// nf = sf; // Error - string ist nicht kompatibel mit number

Type Guards - Typ-Sicherheit zur Laufzeit

Type Guards sind Ausdrücke, die zur Laufzeit prüfen, welchen Typ ein Wert hat. TypeScript versteht diese Prüfungen und verengt den Typ entsprechend.

typeof Type Guards

Hierbei prüft man mit typeof verschiedene Typen und verwendet den Wert dem Typ entsprechend.

TypeScript Beispiel
function processValue(value: string | number) {
    if (typeof value === "string") {
        // In diesem Block ist 'value' ein String
        console.log(value.toUpperCase());
    } else {
        // Hier muss 'value' eine Zahl sein
        console.log(value.toFixed(2));
    }
}

instanceof Type Guards

In diesem Fall wird instanceof verwendet, um den Klassen-Typ zu überprüfen.

TypeScript Beispiel
class Cat {
    meow() { console.log("Meow!"); }
}

class Dog {
    bark() { console.log("Woof!"); }
}

function makeSound(animal: Cat | Dog) {
    if (animal instanceof Cat) {
        // Hier ist animal vom Typ 'Cat'
        animal.meow();
    } else {
        // Hier ist animal vom Typ 'Dog'
        animal.bark();
    }
}

Custom Type Guards

Man kann auch eigene Guards definieren. In diesem Beispiel wird die Technik Type Predicate verwendet. Damit möchte man einen Wert auf einen Typ prüfen. Dabei wird nicht nur ein einfacher boolean zurückgegeben, sondern auch gleichzeitig sichergestellt, dass der Wert von einem bestimmten Typ ist.

TypeScript Beispiel
interface Fish {
    swim(): void;
}

interface Bird {
    fly(): void;
}

const fish: Fish = {
    swim: () => console.log("Fish swimming");
};

const bird: Bird = {
    fly: () => console.log("Bird flying");
};

function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
    if (isFish(pet)) {
        pet.swim();
    } else {
        pet.fly();
    }
}

move(fish);
move(bird);
Output
Fish swimming
Bird flying

In der Funktion isFish() prüft, ob das Tier eine Methode swim() hat. Wenn ja, kann es nur ein Fisch sein. Wenn nein, ist es ein Vogel.

Control Flow Analysis

Control Flow Analysis bezeichnet die Fähigkeit von TypeScript, den tatsächlichen Wert und Typ einer Variable während des Programmablaufs (im Control Flow) zu verfolgen.

TypeScript analysiert den Pfad, den das Programm nimmt (z.B. über if/else, switch, Type Guards), und passt den Typ einer Variable kontextabhängig an.

Beispiel - Type Narrowing durch typeof

Greifen wir das Beispiel von oben auf, da es in diesem Zusammenhang ebenfalls gut passt.

TypeScript Beispiel
function printId(id: number | string) {
    if (typeof id === "string") {
        // Hier ist 'id' ein String
        console.log(id.toUpperCase());
    } else {
        // Hier ist 'id' eine Zahl
        console.log(id.toFixed(2));
    }
}

Im if-Block ist id vom Typ string, im else-Block garantiert vom Typ number.

Beispiel - Type Narrowing durch Null-Check

Hierbei wird null-Prüfung verwendet, um zu überprüfen, ob ein Wert vorhanden ist. Nach dem Check weiß TypeScript, dass name im if-Block kein null mehr sein kann.

TypeScript Beispiel
function greet(name: string | null) {
    if (name) {
        // name ist ein String
        console.log("Hello, " + name.toUpperCase());
    } else {
        // name ist null
        console.log("Hello, stranger");
    }
}

Beispiel - Mehrere Control Flows

TypeScript kombiniert mehrere Bedingungen und verengt die Typen entlang des gesamten Flusses.

TypeScript Beispiel
function example(x: string | number | boolean | null) {
    if (typeof x === "string") {
        // x ist String
        x.toUpperCase();
    } else if (typeof x === "number") {
        // x ist Zahl
        x.toFixed(2);
    } else if (typeof x === "boolean") {
        // x ist ein boolescher Wert
        x.valueOf();
    } else {
        // x ist null
    }
}

Im letzten else kann x NUR noch null sein.

/ Weiter

Zurück zu TypeScript

Zur Übersicht