navigation Navigation


Inhaltsverzeichnis

Primitive Typen


TypeScript bietet ein robustes System von primitiven Datentypen, die als Grundbausteine für komplexere Typdefinitionen dienen. Diese primitiven Typen - string, number, boolean, null, undefined, symbol und bigint - erweitern die Typsicherheit von JavaScript durch präzise Deklarationen. Im Gegensatz zu JavaScript, wo Typen erst zur Laufzeit bestimmt werden, ermöglicht TypeScript die frühzeitige Typprüfung während der Entwicklung. Diese statische Typisierung verhindert häufige Fehler wie unbeabsichtigte Typkonvertierungen und verbessert die Code-Qualität sowie die Wartbarkeit. Die primitiven Typen bilden das Fundament des TypeScript-Ökosystems und sind essentiell für die Entwicklung zuverlässiger und skalierbarer Anwendungen mit klaren Datenstrukturen.

Inhaltsverzeichnis

    Einführung

    Primitive Typen sind die fundamentalen Datentypen, aus denen alle komplexeren Strukturen aufgebaut werden. TypeScript kennt alle JavaScript-Primitives und bietet entsprechende Typen.

    • string: Textdaten
    • number: Numerische Werte (Integer und Float)
    • boolean: Wahrheitswerte
    • null: Absichtlich leerer Wert
    • undefined: Nicht initialisierter Wert
    • symbol: Eindeutige Identifier
    • bigint: Große Ganzzahlen

    String

    Der string Typ repräsentiert Textdaten. In TypeScript gibt es wichtige Nuancen zu beachten.

    Basis Verwendung
    let message: string = "Hello TypeScript";
    let name: string = 'John';
    let template: string = `Welcome, ${name}`;

    In TypeScript gibt es einen Unterschied zwischen string und String. In einem Fall handelt es sich um einen String und im anderen Fall ist es ein Objekt.

    string vs. String
    // Primitiver String
    let primitiveString: string = "Hello";
    
    // String-Objekt
    let objectString: String = new String("Hello");

    Wichtig: Man sollte immer den primitiven String verwenden.


    String Literal Types

    Eine mächtige TypeScript Funktion ist die Möglichkeit, Strings auf spezifische Werte zu beschränken.

    In diesem Beispiel dürfen nur angegebene String-Werte als Parameter der move() Funktion verwendet werden. Alle abweichenden Werte führen zu einem Fehler.

    Beispiel
    type Direction = "north" | "south" | "east" | "west";
    
    function move(direction: Direction) {
        console.log(`Moving ${direction}`);
    }
    
    move("north"); // OK
    move("up"); // Error: Argument of type 'up' is not assignable

    Ebenfalls gibt es die Möglichkeit, die Template Literal Types zu verwenden. Es sind im Grunde zusammengesetzte Typen, die TypeScript auf valide Werte prüfen kann.

    Beispiel
    type EmailDomain = "gmail.com" | "outlook.com" | "company.com";
    type Email = `${string}@${EmailDomain}`;
    
    let validEmail: Email = "user@outlook.com"; // OK
    let invalidEmail: Email = "user@yahoo.com"; // Fehler

    String-Methoden und Type Safety

    TypeScript kennt alle String-Methoden und kann sie entsprechend verwenden.

    Beispiel
    function processText(input: string): string {
        return input
            .trim()
            .toLowerCase()
            .replace(/\s+/g, '-');
    }

    Man kann ebenfalls die sogenannten Type Guards einsetzen, um bestimmte Typen zu prüfen.

    Beispiel
    function isValidEmail(value: unknown): value is string {
        return typeof value === "string" && value.includes("@");
    }

    Number

    Der number Typ umfasst sowohl Ganzzahlen als auch Gleitkommazahlen. JavaScript (und damit auch TypeScript) verwenden IEEE 754 Gleitkomma-Arithmetik.

    Hier ein Beispiel für die Verwendung verschiedener Datenformate.

    Zahlenformate
    let numDecimal: number = 42;
    let numFloat: number = 3.14159;
    let numHex: number = 0xFF; // 255
    let numBinary: number = 0b1010; // 10
    let numOctal: number = 0o744; // 484
    let numScientific: number = 1e5; // 100000

    Die Klasse Number hat bestimmte Grenzen. Diese kann man wie folgt prüfen.

    Grenzen von Number
    console.log(Number.MAX_VALUE);
    console.log(Number.MIN_VALUE);
    console.log(Number.MAX_SAFE_INTEGER);
    console.log(Number.MIN_SAFE_INTEGER);
    1.7976931348623157e+308
    5e-324
    9007199254740991
    -9007199254740991

    Häufige Fallen mit Zahlen

    Gleitkomma-Präzision

    Hier soll man an bestimmten bzw. bei bestimmten Kalkulationen Acht darauf geben, wie die Berechnung in Wirklichkeit aussieht.

    JavaScript (und viele andere Programmiersprachen) verwenden das IEEE 754 Format für Fließkommazahlen (floating point). Dadurch können viele Dezimalzahlen nicht exakt dargestellt werden.

    Ein klassisches Beispiel sieht man direkt hier.

    Beispiel
    console.log(0.1 + 0.2);
    console.log(0.1 + 0.2 === 0.3);
    0.30000000000000004
    false

    Wie man hier sieht, können numerische Vergleiche fehlschlagen und Rundungsfehler sich einschleichen. Besonders bei vielen Berechnungen oder im Finanzbereich.

    ✅ Mögliche Lösung: Toleranzbereich (Epsilon-Vergleich)

    Da exakte Vergleiche problematisch sind, vergleicht man Zahlen, indem man prüft, ob sie “nah genug” beieinander liegen (also ihr Unterschied kleiner als ein definierter Schwellenwert ist).

    Beispiel - Lösung
    true
    false

    Integer-Prüfung

    Im JavaScript-Number-Typ gibt es keinen eigenen Integer-Typ wie in vielen anderen Sprachen - alles ist number (intern 64 Bit float). Es ist oft wichtig zu wissen, ob eine Zahl wirklich eine Ganzzahl ist.

    Beispiel
    function isInteger(value: number): boolean {
        return Number.isInteger(value);
    }
    
    console.log(isInteger(42));
    console.log(isInteger(13.4));
    console.log(isInteger("42"));
    console.log(isInteger(NaN));
    true
    false
    false
    false

    Was macht Number.isInteger()?

    • Prüft, ob der Wert eine echte Ganzzahl ist (also ohne Nachkommastellen)
    • Prüft außerdem, ob der Typ wirklich eine number ist (nicht etwa ein String)

    Sichere Arithmetik

    Was ist mit “sicher” gemeint? Der Wertebereich von number ist begrenzt.

    Es gibt Number.MAX_SAFE_INTEGER (2^53 - 1). Additionen oder Berechnungen können außerhalb dieses Bereichs landen (z.B. bei sehr großen Zahlen), dann sind Rundungsfehler oder gar Unendlichkeiten (Infinity) möglich.

    Beispiel
    function safeAdd(a: number, b: number): number {
        const result = a + b;
    
        if (!Number.isFinite(result)) {
            throw new Error("Das Ergebnis ist nicht endlich");
        }
    
        if (Math.abs(result) > Number.MAX_SAFE_INTEGER) {
            throw new Error("Das Ergebnis geht über den sicheren Bereich hinaus");
        }
    
        return result;
    }
    
    console.log(safeAdd(10, 20));
    
    try {
        console.log(safeAdd(Number.MAX_SAFE_INTEGER, 1));
    } catch (error) {
        console.log(error.message);
    }
    30
    Das Ergebnis geht über den sicheren Bereich hinaus

    Was passiert in diesem Beispiel genau?

    • Prüfung: Ist das Ergebnis endlich?: Falls nicht (Infinity, -Infinity, NaN), wird ein Fehler geworfen.
    • Prüfung: Ist das Ergebnis im sicheren Bereich?: Fall das Ergebnis größer als Number.MAX_SAFE_INTEGER ist, wird ebenfalls ein Fehler geworfen.

    Numeric Literal Types

    Numerisch Literaltypen erlauben es, einen Typ auf konkrete Zahlenwerte einzuschränken, anstatt auf alle Werte vom Typ number.

    Beispiel
    type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
    
    function rollDice(): DiceRoll {
        return Math.floor(Math.random() * 6 + 1) as DiceRoll;
    }
    
    console.log(rollDice());
    console.log(rollDice());
    console.log(rollDice());
    5
    3
    4

    Der Typ DiceRoll kann nur einen der Werte 1, 2, 3, 4, 5 oder 6 annehmen - nichts anderes. Das ist sogenannter Union Type - eine Vereinigung mehrerer konkreter Werte.


    Branded Types

    Ein Branded Type ist eine Technik, um primitive Typen (wie number) mit einer unsichtbaren “Markierung” (Brand) zu versehen. Diese Markierung gibt es nur auf Typ-Ebene - im JavaScript Code gibt es keine Auswirkung, aber der Compiler kann die Typen unterscheiden.

    Beispiel
    type Age = number & { __brand: "age" };

    Age ist ein Typ, der einerseits eine Zahl ist (number), aber zusätzlich das Brand-Tag { __brand: "age" } trägt. Das sorgt dafür, dass man einen Wert **nur als Age nutzen kann, wenn er vorher “gebranded” wurde.

    Nun bauen wir das Beispiel etwas weiter aus und erzeugen eine Factory-Funktion zum Erzeugen eines Age-Wertes.

    Beispiel (Ausbau)
    type Age = number & { __brand: "age" };
    
    function createAge(value: number): Age {
        if (value < 0 || value > 150) {
            throw new Error("Invalid age");
        }
    
        return value as Age;
    }

    Hiermit haben wir Folgendes:

    • Nur wenn die Zahl im gültigen Bereich liegt, darf sie als Age verwendet werden.
    • as Age setzt das “Brand” auf Typ-Ebene.

    Nun kann man jetzt verlangen, dass überall im Code, wo ein Age benötigt wird, nur von createAge() geprüfte Werte übergeben werden können.

    Bauen wir das Beispiel noch ein wenig aus, um etwas Praxisbezug zu erhalten.

    Beispiel (Ausbau)
    type Age = number & { __brand: "age" };
    
    function createAge(value: number): Age {
        if (value < 0 || value > 150) {
            throw new Error("Invalid age");
        }
    
        return value as Age;
    }
    
    function registerPerson(name: string, age: Age) {
        console.log(`Person: ${name} hat einen gültigen Alter von ${age}`);
    }
    
    registerPerson("John", createAge(42));
    registerPerson("Alice", 212);

    Wenn wir diesen Code ausführen, erhalten wir einen Fehler, da 212 nicht über createAge() Funktion erzeugt/geprüft wurde.

    error TS2345: Argument of type 'number' is not assignable to parameter of type 'Age'.
    Type 'number' is not assignable to type '{ __brand: "age"; }'.

    Boolean

    Werte

    Der boolean Typ ist der einfachste Primitive, aber seine korrekte Verwendung ist entscheidend.

    Der boolean Typ kann nur zwei Werte annehmen: true oder false.

    let isActive: boolean = true;
    let isComplete: boolean = false;

    Boolean vs. Truthy/Falsy

    In JavaScript kann in Bedingungen (if, while, Ternary, …) jede beliebige Variable verwendet werden, nicht nur echte Booleans. Das führt dazu, dass nicht nur true oder false, sondern auch andere Werte als “wahr” oder “falsch” interpretiert werden.

    Falsy Werte (in JavaScript) Folgende Werte gelten als falsch in Bedingungen:

    • false
    • 0 und -0
    • "" (leerer String)
    • null
    • undefined
    • NaN

    Alle anderen Werte sind “truthy” (werden als wahr behandelt).

    Werte können nach Boolean hauptsächlich auf zwei unterschiedliche Arten konvertiert werden.

    // Methode 1
    const isBoolean: boolean = Boolean(value);
    
    // Methode 2
    const isTruthy: boolean = !!value;
    Beispiel - Methode 1
    console.log(Boolean(0));
    console.log(Boolean("Hallo"));
    console.log(Boolean(""));
    console.log(Boolean({}));
    console.log(Boolean([]));
    console.log(Boolean(-12));
    false
    true
    false
    true
    true
    true
    Beispiel - Methode 2
    function checkTruthy(value: any): boolean {
        const result: boolean = !!value;
        return result;
    }
    
    console.log(checkTruthy(0));
    console.log(checkTruthy("Hallo"));
    console.log(checkTruthy(""));
    console.log(checkTruthy({}));
    console.log(checkTruthy([]));
    console.log(checkTruthy(-12));
    false
    true
    false
    true
    true
    true

    Boolean Literal Types

    Die Boolean Literal Types werden folgendermaßen erstellt.

    type Yes = true;
    type No = false;
    type Answer = Yes | No;

    Damit kann man einen Typ auf einen konkreten Wahrheitswert beschränken. Answer kann nur true oder false sein - das ist praktisch, wenn man z.B. Antworten, Status oder Enums modellieren möchte, die wirklich nur exakt diese Werte erlauben sollen.

    Beispiel (Ausbau)
    type Yes = true;
    type No = false;
    type Answer = Yes | No;
    
    let userAgreed: Answer = true; // Ok
    userAgreed = false; // Ok
    userAgreed = 1; // Fehler

    Feature Flags

    Hierbei handelt es sich eigentlich um bestimmte Interfaces, welche verschiedene Werte vom Typ boolean beinhalten und als eine Art Schalter wirken können.

    interface FeatureFlags {
        darkMode: boolean;
        betaFeatures: boolean;
        debugMode: boolean;
    }

    Dieses Interface beschreibt ein Objekt mit verschiedenen Schaltern (Features), die aktiviert (true) oder deaktiviert (false) sein können. Solche Feature Flags werden oft zur Steuerung von Funktionen, Debugging oder Sonstigem eingesetzt.

    Hier ein praktisches Beispiel.

    Beispiel
    interface FeatureFlags {
        darkMode: boolean;
        betaFeatures: boolean;
        debugMode: boolean;
    }
    
    function initFeatures(debug: boolean = false): FeatureFlags {
        return {
            darkMode: localStorage.getItem("darkMode") === "true",
            betaFeatures: process.env.NODE_ENV === "development",
            debugMode: debug
        };
    }
    
    const features = initFeatures(true);
    
    if (features.darkMode) { /* ... */ }
    if (features.betaFeatures) { /* ... */ }
    if (features.debugMode) { /* ... */ }

    Null

    JavaScript, und damit TypeScript, kennt zwei Arten von “Nichts”.

    • undefined: Das Fehlen eines Wertes (z.B. eine nicht initialisierte Variable, fehlender Rückgabewert, optionale Properties)
    • null: Ein explizit gesetzter “leer” Wert, um etwas absichtlich als “nichts” zu kennzeichnen

    Beide sind primitive Datentypen.

    let a: undefined = undefined;
    let b: null = null;

    Unterschiede

    undefined

    • Wert existiert nicht
    • Für optionale Properties oder nicht initialisierte Variablen

    null

    • Wert existiert, ist leer
    • Absichtlich “leer” gesetzt (z.B. keine Referenz mehr vorhanden)

    TypeScript zwingt einen mit den richtigen Einstellungen (strictNullChecks) dazu, explizit zu entscheiden, ob ein Wert null und/oder undefined sein kann.

    Beispiel
    // Muss undefined sein
    let notInitialized: undefined = undefined;
    
    // Muss null sein
    let explicitNull: null = null;
    
    // Kann string oder null sein
    let value: string | null = null;
    
    // Kann number oder undefined sein
    let maybeNumber: number | undefined;

    Ein Unterschied stellt die aktiviert Compiler-Option strictNullChecks dar.

    • Ohne strictNullChecks: Jede Variable kann automatisch auch null oder undefined sein
    • Mit strictNullChecks: (Standard in modernen Projekten) Man muss explizit festelegen, wenn null/undefined erlaubt ist - andernfalls ist ein Wert immer vorhanden.

    Anwendungsfälle

    Optionale Properties (undefined)

    Im folgenden Beispiel ist email nicht verpflichtend. Fehlt es, ist der Wert undefined.

    interface User {
        id: string;
        name: string;
        email?: string; // Kann string oder undefined sein
    }

    Nullable Fields (null)

    In diesem Beispiel drückt deletedAt einen Zustand aus “wurde gelöscht”. Der Wert ist bewusst null.

    interface User {
        id: string;
        name: string;
        deletedAt: Date | null; // Ein Datum oder explizit "gelöscht" (null)
    }

    Typ Guards (sichere Verarbeitung)

    Wenn man Werte zulässt, die null oder undefined sein können, muss man prüfen, bevor man darauf zugreift.

    function getLength(str: string | null | undefined): number {
        if (str === null || str === undefined) {
            return 0;
        }
    
        // Sicherer Zugriff, da jetzt garantiert string
        return str.length;
    }

    Symbol

    Symbol ist ein primitiver Datentyp, der mit ES6 (ECMAScript 2015) eingeführt wurde.

    Folgende Punkte machen ein Symbol aus:

    • Ein Symbol ist ein einzigartiger und unveränderlicher Wert
    • Es wird vor allem genutzt, um eindeutige Schlüssel für Objekte zu erstellen
    • Symbole sind nicht als Eigenschaftsnamen für “normale” Iteration sichtbar (z.B. for ... in, Object.keys())

    Ein Symbol kann wie folgt erstellt werden.

    const sym1 = Symbol("key");
    const sym2 = Symbol("key");
    console.log(sym1 === sym2); // false - Immer eindeutig

    Symbol("key") erzeugt ein neues, eindeutiges Symbol. Der String "key" ist nur eine Beschreibung (zum Debuggen), nicht die Identität des Symbols. Auch wenn man zwei Symbole mit demselben Beschreibungstext erzeugt, sind sie immer unterschiedlich.


    Globale Symbole

    Es besteht auch die Möglichkeit, globale Symbole zu erzeugen. Dabei wird auf ein globales Symbol-Register zugegriffen.

    Man bekommt immer dasselbe Symbol zurück für denselben Schlüssel.

    Diese Art von Symbolen ist ideal, wenn man ein Symbol an mehreren Stellen im Programm/Modul wiederverwenden möchte.

    Globale Symbole
    const globalSym1 = Symbol.for("app.key");
    const globalSym2 = Symbol.for("app.key");
    
    console.log(globalSym1 === globalSym2); // true

    Wichtig

    • Mit Symbol() erzeugte Symbole sind nie gleich, auch bei gleichem Beschreibungstext.
    • Mit Symbol.for() erzeugte Symbole mit gleichem Schlüssel sind immer gleich.

    Warum Symbole für Properties?

    • Sie sind nicht versehentlich überschreibbar
    • Sie sind nicht sichtbar beim normalen Iterieren
    • Sie verhindern Namenskollisionen, z.B. in Libraries oder Frameworks
    Beispiel
    class ApiClient {
    
        private [Symbol.toStringTag] = "ApiClient";
        private static readonly SECRET_KEY = Symbol("secretKey");
    
        private [ApiClient.SECRET_KEY]: string;
    
        constructor(secret: string) {
            this[ApiClient.SECRET_KEY] = secret;
        }
    
    }

    BigInt

    BigInt wurde für Zahlen außerhalb des sicheren Integer-Bereiches eingeführt. JavaScript number kann nur ganzzahlige Werte im Bereich ±(2^53 - 1) exakt darstellen. Der Grenzwert ist die Konstante Number.MAX_SAFE_INTEGER (9007199254740991). Alles darüber hinaus kann zu Rundungsfehlern führen.

    Das kann ein Problem in folgenden Fällen darstellen:

    • IDs und Counter mit vielen Ziffern
    • Sehr große Beträge (z.B. Cent-Beträge als Integer)
    • Zeitstempel mit hoher Präzision (Microsekunden)
    • Kryptografie
    Beispiel für Fehlerbereich
    console.log(Number.MAX_SAFE_INTEGER + 1);
    console.log(Number.MAX_SAFE_INTEGER + 2);
    9007199254740992
    9007199254740992

    Wie man an diesem Beispiel sieht, liefert die Addition mit zwei unterschiedlichen Werten dasselbe Ergebnis.


    Seit ES2020 gibt es den Typ BigInt, der beliebig große Ganzzahlen exakt darstellen kann.

    Folgendermaßen kann eine Zahl vom Typ BigInt erstellt werden.

    // Via Funktion
    const b1: bigint = BigInt(9007199254740992);
    
    // Via 'n' Suffix (Literal)
    const b2: bigint = 9007199254740993n;

    Es besteht auch die Möglichkeit mithilfe des BigInt Konstruktors eine große Zahl aus einem String zu erstellen.

    const b1: bigint = BigInt("87987234987234987234");

    Operationen mit BigInt Typen sind nur zwischen BigInts erlaubt. Das Mischen von BigInt und Number führt zu einem TypeError.

    Beispiel für Addition
    const b1: bigint = BigInt(234898234098234989);
    const b2: bigint = BigInt(989832987237987234);
    const sum = b1 + b2;
    console.log(sum);
    1224731221336222176n

    Konvertierung

    Für Konvertierung von anderen Werten nach BigInt müssen ein paar Punkte beachtet werden.

    • Bei number unbedingt prüfen: Es darf nur eine Ganzzahl sein (kein Float)
    • Strings dürfen nur ganze Zahlen enthalten, sonst gibt es einen Fehler

    Schauen wir uns ein praktisches Beispiel an.

    Beispiel - Konvertierung
    function convertToBigInt(value: number | string): bigint {
        if (typeof value === "number") {
            if (!Number.isInteger(value)) {
                throw new Error("Cannot convert float to BigInt");
            }
        }
    
        return BigInt(value);
    }
    
    const r1 = convertToBigInt(123);
    console.log(r1);
    
    const r2 = convertToBigInt("9000");
    console.log(r2);
    
    try {
        const r3 = convertToBigInt(2.5);
    } catch (error) {
        console.log(error.message);
    }
    123n
    9000n
    Cannot convert float to BigInt