Primitive vs. Object Types: Die Grundlagen des Typsystems
<T>
type
TypeScript · Typsystem · Strict Mode · Tooling
Primitive vs. Object Types: Die Grundlagen des Typsystems
Warum TypeScript anders "denkt" als PHP

Wer TypeScript nur als PHP mit Typen begreift, stolpert schnell über strukturelle Typprüfung, Referenzsemantik bei Objekten und fehlende Nominal-Typisierung. Dieser Artikel erklärt den fundamentalen Unterschied zwischen primitiven und Object Types, zeigt TypeScripts Duck-Typing-Philosophie im direkten Vergleich zu PHP und macht die häufigsten Denkfehler beim Umstieg sichtbar.

16 Min. Lesezeit Primitives · Objects · Structural Typing TypeScript 5.x · strict Mode · PHP-Vergleich

1. Warum primitive und Object Types den Kern des Typsystems bilden

Das Typsystem von TypeScript basiert auf einer einzigen fundamentalen Unterscheidung: primitive Types gegenüber Object Types. Zur ersten Gruppe zählen string, number, boolean, null, undefined, symbol und bigint, zur zweiten alles andere, also Plain Objects, Arrays, Funktionen und Klasseninstanzen. Diese Trennung ist keine akademische Fußnote, sondern bestimmt direkt, wie Werte im Speicher abgelegt, kopiert, verglichen und mutiert werden. TypeScript fügt dieser bereits in JavaScript vorhandenen Unterscheidung lediglich eine statische Prüfschicht hinzu, ändert aber nichts an den zugrunde liegenden Laufzeit-Regeln.

Für PHP-Entwickler, die TypeScript für Build-Skripte, Headless-Frontends oder API-Clients einsetzen, verschiebt sich dabei das mentale Modell von "Typ" spürbar. PHP prüft Typdeklarationen zur Laufzeit über strict_types einheitlich für Skalare und Objekte, während TypeScripts Typen beim Kompilieren vollständig verschwinden (Type Erasure) und ausschließlich als Entwicklerwerkzeug existieren. Wer primitive und Object Types wie gleichwertige, austauschbare "Typen" im PHP-Sinn behandelt, übersieht schnell, warum zwei scheinbar identische Objekte nicht gleich sind oder warum eine Kopie plötzlich Seiteneffekte auslöst.

2. Die primitiven Typen im Überblick: string, number, boolean & Co.

Die sieben primitiven Typen in TypeScript sind string, number, boolean, null, undefined, symbol und bigint. Anders als in vielen anderen Sprachen gibt es keinen separaten int- oder float-Typ, number deckt sowohl Ganzzahlen als auch Kommazahlen ab und basiert intern auf IEEE-754-Fließkommazahlen mit doppelter Genauigkeit. Für Ganzzahlen jenseits von Number.MAX_SAFE_INTEGER (2^53 minus 1) steht seit ES2020 bigint zur Verfügung, erkennbar am n-Suffix wie in 9007199254740993n. bigint und number lassen sich nicht ohne explizite Konvertierung mischen, TypeScript meldet das als Fehler.

null und undefined sind eigenständige primitive Typen mit jeweils genau einem Wert. Mit aktivierter strictNullChecks-Option (Teil von strict: true) müssen sie explizit in der Typsignatur auftauchen, etwa als string | null, sonst lehnt der Compiler die Zuweisung ab. symbol erzeugt garantiert eindeutige, unveränderliche Werte, häufig verwendet als Objekt-Property-Keys, die nicht mit Zeichenketten kollidieren können. Alle primitiven Typen teilen zwei Eigenschaften: Sie sind unveränderlich und werden mit === nach Inhalt statt nach Referenz verglichen.


// The seven primitive types in TypeScript
let userName: string = "Miron";
let age: number = 42;
let price: number = 19.99;          // no separate "int"/"float" - just number
let isActive: boolean = true;
let middleName: string | null = null;
let nickname: string | undefined = undefined;
let userId: symbol = Symbol("user-id");
let bigCounter: bigint = 9_007_199_254_740_993n; // beyond Number.MAX_SAFE_INTEGER

// Primitives are compared by value
console.log(age === 42);            // true
console.log("a" + "b" === "ab");    // true

// Primitives are immutable - operations always return a new value
let original = "hello";
let shouted = original.toUpperCase();
console.log(original);              // "hello" - unchanged
console.log(shouted);               // "HELLO" - a new string

3. Object Types: Objekte, Arrays, Funktionen und Klassen

Alles, was kein primitiver Typ ist, gilt in TypeScript als Object Type: Plain Objects, Arrays (intern Array<T>), Funktionen, Klasseninstanzen und eingebaute Objekte wie Date, Map, Set oder RegExp. Der entscheidende Unterschied zu Primitiven liegt in der Referenzsemantik: Eine Variable speichert nicht den Objektinhalt selbst, sondern nur einen Verweis auf dessen Speicherort im Heap. Wird dieser Verweis einer zweiten Variable zugewiesen, zeigen beide Variablen auf exakt dasselbe Objekt im Speicher, nicht auf zwei unabhängige Kopien.

Diese Referenzsemantik ist eine häufige Fehlerquelle beim Umstieg aus PHP, wo Objects standardmäßig ebenfalls per Referenz gehandhabt werden, Arrays aber per Value kopiert werden, sobald sie einer neuen Variable zugewiesen werden. In TypeScript und JavaScript gilt für Arrays dieselbe Referenzsemantik wie für alle anderen Object Types. Eine Mutation über push(), splice() oder eine direkte Indexzuweisung verändert das ursprüngliche Array überall dort, wo es referenziert wird, auch in einem State-Management-Objekt oder einer Alpine.js-Datenstruktur, die dieselbe Referenz hält.


// Object types are reference types
interface CartItem {
  sku: string;
  quantity: number;
}

const itemA: CartItem = { sku: "MS-1234", quantity: 1 };
const itemB = itemA; // copies the reference, not the object

itemB.quantity = 5;
console.log(itemA.quantity); // 5 - itemA "changed" too, same object in memory

// Arrays, functions and class instances are all object types under the hood
console.log(typeof [1, 2, 3]);      // "object"
console.log(typeof (() => {}));     // "function" (a special case, but still a reference type)
console.log(typeof new Date());     // "object"

// Independent copy requires an explicit clone
const itemC: CartItem = { ...itemA };            // shallow copy
const itemD: CartItem = structuredClone(itemA);  // deep copy

4. Value-Semantik vs. Reference-Semantik: Kopieren, Vergleichen, Mutieren

Der Operator === verhält sich für primitive und Object Types fundamental unterschiedlich. Zwei Strings mit identischem Inhalt sind immer ===, weil Primitive nach Wert verglichen werden. Zwei strukturell identische Objekte, etwa { sku: 'MS-1' } und { sku: 'MS-1' }, sind dagegen niemals === zueinander, außer es handelt sich um dieselbe Referenz im Speicher. Diese Regel gilt unabhängig davon, wie exakt der Objektinhalt übereinstimmt, TypeScript prüft das zur Compile-Zeit nicht anders, als JavaScript es zur Laufzeit tut.

Für inhaltliche Vergleiche braucht es explizite Deep-Equal-Prüfungen, etwa über eine Bibliothek wie lodash oder einfache JSON.stringify()-Vergleiche bei unkomplizierten Strukturen. Auch die readonly-Modifikator-Annotation täuscht hier leicht: Sie verhindert lediglich zur Compile-Zeit, dass eine Property neu zugewiesen wird, verschwindet aber beim Kompilieren nach JavaScript vollständig und bietet keinerlei Laufzeitschutz. Echte Unveränderlichkeit erfordert Object.freeze(), das jedoch nur eine flache Sperre setzt, verschachtelte Objekte innerhalb eines eingefrorenen Objekts bleiben weiterhin veränderbar.

5. Structural Typing: TypeScripts Kernphilosophie (Duck Typing)

TypeScripts Kernphilosophie ist structural typing, umgangssprachlich Duck Typing genannt: "Wenn es aussieht wie eine Ente und quakt wie eine Ente, ist es eine Ente." Zwei Typen gelten als kompatibel, sobald sie dieselbe Form besitzen, also dieselben Properties mit kompatiblen Typen aufweisen, unabhängig davon, ob sie über eine gemeinsame Klasse, ein implementiertes Interface oder gar keine explizite Beziehung verfügen. Das steht in scharfem Kontrast zu nominal typisierten Sprachen wie PHP, Java oder C#, wo Typkompatibilität an Klassennamen und deklarierte Vererbungshierarchien gebunden ist.

In der Praxis bedeutet das: Eine Funktion, die einen Parameter vom Typ eines Interfaces erwartet, akzeptiert jedes Objekt mit passender Form, egal ob es aus einer Klasseninstanz, einem Objektliteral oder dem Rückgabewert einer völlig anderen Funktion stammt. Das macht Composition, Mocking in Tests und die Integration unabhängiger Module deutlich einfacher, weil keine explizite Vererbungsbeziehung hergestellt werden muss, nur die Form muss passen.


// Structural typing: shape decides compatibility, not class identity or inheritance
interface Loggable {
  log(): string;
}

class OrderConfirmation {
  constructor(private orderId: string) {}
  log(): string {
    return `Order confirmed: ${this.orderId}`;
  }
}

class DeploymentEvent {
  constructor(private version: string) {}
  log(): string {
    return `Deployed version ${this.version}`;
  }
}

// Both classes are unrelated - no shared base class, no "implements Loggable"
function writeToAuditLog(entry: Loggable): void {
  console.log(entry.log());
}

writeToAuditLog(new OrderConfirmation("ORD-1001")); // works
writeToAuditLog(new DeploymentEvent("2.4.8-p4"));    // also works - same shape is enough

// A plain object literal satisfies the interface too, no class required at all
writeToAuditLog({ log: () => "Manual log entry" }); // works

6. Nominal Typing: Die Erwartungshaltung aus PHP und wie sie in die Irre führt

PHP-Entwickler bringen fast automatisch die Erwartung mit, dass zwei Klassen mit identischen Properties trotzdem inkompatibel bleiben, solange sie nicht über Vererbung oder ein gemeinsames Interface verbunden sind, so wie es instanceof in PHP prüft. TypeScript verhält sich hier anders: Wenn zwei Klassen wie UserId und ProductId beide nur ein einziges readonly value: string-Property besitzen, sind sie strukturell identisch, und TypeScript lässt eine Zuweisung zwischen ihnen zu, sobald eine Type Assertion oder eine unsauber typisierte Stelle im Code das erzwingt.

Dieser stille Kategorienfehler fällt besonders bei ID-Typen, Geldbeträgen oder Maßeinheiten ins Gewicht, wo eine Verwechslung schwerwiegende, aber vom Compiler nicht erkannte Bugs erzeugt. Die Lösung heißt Branded Types (auch Opaque Types genannt): Ein zusätzliches, zur Laufzeit nicht existierendes Marker-Property wird per Intersection-Type an den zugrunde liegenden primitiven Typ angehängt. Ab diesem Zeitpunkt lehnt der Compiler die Vermischung strukturell ähnlicher, aber semantisch unterschiedlicher Werte zuverlässig ab, ganz ohne Laufzeit-Overhead.


// The PHP habit: expecting class identity to matter, like instanceof in PHP
class UserId {
  constructor(public readonly value: string) {}
}

class ProductId {
  constructor(public readonly value: string) {}
}

function findUser(id: UserId): void {
  console.log(`Looking up user ${id.value}`);
}

// This looks safe, but structurally UserId and ProductId are identical:
// both are "{ readonly value: string }" - TypeScript allows the mix-up silently
// if we accidentally construct the wrong wrapper or skip the constructor
const looksLikeUserId = { value: "PROD-42" } as UserId; // compiles, wrong at runtime

// Fix: brand the type so structurally identical shapes stop being interchangeable
type Branded<T, Brand extends string> = T & { readonly __brand: Brand };
type SafeUserId = Branded<string, "UserId">;
type SafeProductId = Branded<string, "ProductId">;

function toSafeUserId(value: string): SafeUserId {
  return value as SafeUserId;
}

function findUserSafe(id: SafeUserId): void {
  console.log(`Looking up user ${id}`);
}

// findUserSafe(toSafeProductId("PROD-42")); // now a compile error - brands differ
findUserSafe(toSafeUserId("USR-1"));           // correct usage

7. Type Guards: typeof, instanceof und benutzerdefinierte Guards

typeof funktioniert zuverlässig nur für primitive Typen plus die Sonderfälle "function" und "object", liefert für Arrays, Klasseninstanzen und die meisten eingebauten Objekte aber einheitlich "object" zurück und eignet sich daher nicht zur Unterscheidung einzelner Objektformen. instanceof prüft stattdessen die Prototype-Kette und funktioniert zuverlässig bei Klasseninstanzen, versagt aber bei reinen Objektliteralen oder Objekten, die über Object.create() erzeugt wurden. Array.isArray() ist die korrekte, robuste Methode, um Arrays von anderen Object Types zu unterscheiden.

Für komplexere Objektformen, insbesondere solche, die von außen kommen wie API-Antworten oder JSON.parse()-Ergebnisse, reichen eingebaute Operatoren nicht aus. Benutzerdefinierte Type Guards mit is-Prädikat-Rückgabetyp (function isProduct(x: unknown): x is Product) kombinieren eine echte Laufzeitprüfung mit einer Typverengung, die der Compiler anschließend im gesamten folgenden Codeblock respektiert. Weil TypeScript strukturell typisiert, aber zur Laufzeit keine Garantie für die tatsächliche Form externer Daten gibt, sind solche Guards, oft in Kombination mit Validierungsbibliotheken wie zod, die einzige verlässliche Brücke zwischen angenommenem und tatsächlichem Objektaufbau.

8. interface vs. type: Objektformen deklarieren

Sowohl interface als auch type können Object Types beschreiben, unterscheiden sich aber in wichtigen Details. interface ist offen für Declaration Merging: Mehrere interface Product { ... }-Deklarationen mit demselben Namen werden automatisch zu einer einzigen zusammengeführt, ein Feature, das Bibliotheksautoren nutzen, um bestehende Typen zu erweitern. type ist dagegen geschlossen, kann aber Unions, Intersections, Mapped Types und Conditional Types ausdrücken, Konstrukte, die mit interface allein nicht möglich sind.

Für reine Objektformen behandelt TypeScript beide Varianten strukturell vollkommen gleichwertig, es spielt für die Zuweisungskompatibilität keine Rolle, ob eine Form über interface oder type deklariert wurde. Als Faustregel hat sich etabliert: interface für öffentliche, potenziell erweiterbare Vertragstypen einer Library oder eines Moduls, type für Unions, Utility-Kompositionen und alles, was interface syntaktisch nicht abbilden kann. Eine Klasse, die implements Product deklariert, bekommt dabei nur eine zusätzliche lokale Prüfung an der Klassendefinition, überall sonst im Code bleibt die Instanz rein strukturell kompatibel.


// interface: open, supports declaration merging, ideal for extendable contracts
interface Product {
  sku: string;
  price: number;
}

interface Product {
  currency: string; // merges into the same Product interface automatically
}

// type: closed, but supports unions, intersections and mapped types
type DiscountedProduct = Product & { discountPercent: number };
type PaymentMethod = "invoice" | "creditcard" | "paypal"; // interfaces cannot express unions

// Both are structurally identical for assignability - TypeScript does not
// care whether the shape came from "interface" or "type"
const p: Product = { sku: "MS-1234", price: 49.9, currency: "EUR" };
const d: DiscountedProduct = { ...p, discountPercent: 10 };

class InvoiceProduct implements Product {
  constructor(public sku: string, public price: number, public currency: string) {}
}

// A class instance is still just structurally checked everywhere outside "implements"
const anotherProduct: Product = new InvoiceProduct("MS-5678", 29.9, "EUR");

9. Typische Fallstricke und Best Practices im Vergleich

Die folgende Übersicht stellt naive, aus anderen Sprachen mitgebrachte Erwartungen dem tatsächlichen Verhalten von TypeScript gegenüber. Wer diese fünf Punkte verinnerlicht, vermeidet die häufigsten Bugs, die aus der Verwechslung von primitiven und Object Types entstehen.

Kriterium Naive Erwartung Tatsächliches Verhalten Praxis-Konsequenz
Vergleich (===) Zwei Objekte mit gleichem Inhalt sind gleich Objekte vergleichen nach Referenz, nicht nach Inhalt Deep-Equal-Prüfung oder structuredClone nötig
Mutability Number/String lassen sich "in-place" verändern Primitive sind immutable, jede Änderung erzeugt einen neuen Wert const ist bei Primitiven sicher, bei Objekten nicht
Kopierverhalten let b = a kopiert den Objektinhalt Zuweisung kopiert nur die Referenz Spread oder structuredClone für echte Kopien
typeof-Ergebnis typeof liefert für jede Klasse einen eigenen String typeof liefert für Objekte, Arrays und Klassen einheitlich "object" instanceof oder Duck-Typing-Checks nötig
Typkompatibilität Zwei Klassen mit identischen Properties sind inkompatibel TypeScript erlaubt Zuweisung bei gleicher Struktur Branded Types nötig, wenn Nominal-Verhalten gewünscht ist

In der Praxis zeigen sich diese Unterschiede besonders an den Nahtstellen zu externen Daten: API-Antworten, JSON.parse()-Ergebnisse oder Formulareingaben tragen zur Laufzeit keine TypeScript-Typinformationen mehr, weil diese beim Kompilieren vollständig entfernt werden. Wer Value- und Reference-Semantik sauber trennt und structural statt nominal denkt, schreibt robusteren Code, unabhängig davon, ob dieser Code am Ende ein Magento-Build-Skript, ein Headless-Frontend oder eine API-Integration betrifft.

Mironsoft

TypeScript-Tooling, Headless-Integrationen und Type-Safety für Magento- und Hyvä-Projekte

TypeScript-Typsystem sauber im Team einsetzen?

Wir analysieren bestehende TypeScript-Codebasen, decken strukturelle Typfehler und fehlende Laufzeit-Validierung auf und richten Build-Skripte, API-Clients und Headless-Frontends auf ein robustes, konsistentes Typsystem aus.

Type-System-Audit

Strukturelle Typfehler, unsichere Assertions und fehlende Branded Types aufspüren

Laufzeit-Validierung

zod-Schemas für API-Grenzen, damit Typversprechen auch zur Laufzeit gelten

Team-Coaching

Structural Typing, strict Mode und Best Practices praxisnah für PHP-Teams vermittelt

10. Zusammenfassung

Primitive Types (string, number, boolean, null, undefined, symbol, bigint) sind unveränderlich und werden nach Wert verglichen, Object Types (Objekte, Arrays, Funktionen, Klasseninstanzen) sind veränderlich und werden nach Referenz verglichen. Diese eine Unterscheidung erklärt fast jedes Verhalten, das beim Umstieg von PHP nach TypeScript zunächst überrascht: warum zwei inhaltlich gleiche Objekte nicht === sind, warum eine Kopie plötzlich das Original mitverändert, und warum readonly keine echte Laufzeit-Immutability erzeugt.

Die zweite, ebenso wichtige Unterscheidung ist structural statt nominal typing: TypeScript prüft Kompatibilität anhand der Form eines Werts, nicht anhand seines Klassennamens oder seiner Vererbungshierarchie. Das macht Code flexibler und einfacher zu testen, öffnet aber auch die Tür für stille Kategorienfehler, etwa wenn ID-Typen versehentlich vertauscht werden. Branded Types, benutzerdefinierte Type Guards und Laufzeit-Validierung mit Bibliotheken wie zod schließen genau diese Lücke zwischen Compile-Zeit-Sicherheit und tatsächlicher Laufzeit-Realität.

Primitive vs. Object Types - Das Wichtigste auf einen Blick

Primitive Types

Sieben Typen (string, number, boolean, null, undefined, symbol, bigint). Unveränderlich, Vergleich nach Wert.

Object Types

Objekte, Arrays, Funktionen, Klasseninstanzen. Veränderlich, Vergleich nach Referenz im Heap.

Structural Typing

TypeScripts Kernphilosophie: Die Form eines Werts entscheidet, nicht Klassenname oder Vererbung.

PHP-Fallstricke

Nominal-Erwartung führt in die Irre. Branded Types und zod-Validierung schließen die Lücke.

11. FAQ: Primitive vs. Object Types

1Was ist der Unterschied zwischen primitiven und Object Types?
Primitive sind unveränderlich und werden nach Wert verglichen. Object Types sind veränderlich und werden nach Referenz verglichen, weil eine Variable nur auf den Speicherort im Heap zeigt.
2Warum sind primitive Typen immutable und Objects nicht?
Primitive liegen im Stack, jede Operation erzeugt einen neuen Wert. Objects leben im Heap, Methoden und Zuweisungen verändern denselben referenzierten Speicherbereich.
3Warum liefert typeof für Arrays und Klassen "object"?
typeof unterscheidet nur JavaScript-Grundkategorien. Arrays, Objekte und Klasseninstanzen geben alle "object" zurück. Array.isArray() oder instanceof helfen bei feinerer Unterscheidung.
4Was bedeutet structural typing (Duck Typing)?
Zwei Typen sind kompatibel, wenn sie dieselbe Form haben, unabhängig von Klassenname oder Vererbung. Wenn es aussieht und sich verhält wie eine Ente, ist es eine Ente.
5Warum akzeptiert TypeScript identische Klassen als kompatibel?
Weil die Typprüfung strukturell arbeitet, nicht nominal wie in PHP. Nur die tatsächliche Objektform zählt für die Zuweisungskompatibilität, das ist beabsichtigtes Design.
6Wie simuliere ich nominal typing mit Branded Types?
Mit einem zusätzlichen Brand-Property per Intersection-Type, etwa string & { readonly __brand: 'UserId' }. Der Compiler verweigert dann die Vermischung strukturell ähnlicher Werte.
7interface vs. type für Objektformen: Was ist der Unterschied?
interface unterstützt Declaration Merging, type kann Unions und Mapped Types ausdrücken. Für reine Objektformen sind beide strukturell gleichwertig.
8Wie vergleiche ich zwei Objekte inhaltlich statt per Referenz?
Mit einer Deep-Equal-Funktion wie lodash isEqual oder JSON.stringify()-Vergleich bei einfachen Strukturen. === bleibt bei Objects immer ein reiner Referenzvergleich.
9Erzeugt readonly echte Immutability?
Nein, readonly ist reine Compile-Zeit-Prüfung ohne Laufzeitwirkung. Object.freeze() erzeugt echten, aber nur flachen Laufzeitschutz.
10Warum brauche ich trotz Typen Laufzeit-Validierung?
TypeScript-Typen werden beim Kompilieren entfernt (Type Erasure). Externe Daten wie API-Antworten müssen mit Bibliotheken wie zod zur Laufzeit validiert werden.