TypeScript für PHP-Entwickler: Konzepte im Vergleich
<T>
type
TypeScript · PHP · Typsysteme · Konzepte
TypeScript für PHP-Entwickler
Konzepte im Vergleich

PHP-Entwickler kennen die meisten Konzepte von TypeScript bereits, nur unter anderen Namen und mit anderen Garantien. Dieser Artikel ordnet Union Types, Interfaces, Immutability, Enums und Generics aus der PHP-Welt ihren TypeScript-Entsprechungen zu, zeigt konkret, wo nominale und strukturelle Typisierung auseinanderlaufen, und vergleicht ein typisches Value Object Zeile für Zeile in beiden Sprachen.

14 Min. Lesezeit Union Types · Interfaces · Generics PHP 8.4 · TypeScript 5 · PHPStan

1. Warum PHP-Entwickler TypeScript schneller lernen als gedacht

Wer aus der PHP-Welt kommt und sich zum ersten Mal mit TypeScript beschäftigt, unterschätzt meist, wie viel Vorwissen schon vorhanden ist. Seit PHP 7.0 gibt es Skalar-Typdeklarationen, seit PHP 8.0 Union Types, seit PHP 8.1 Enums und Readonly-Properties, seit PHP 8.2 auch Readonly-Klassen. Wer moderne PHP-Codebasen mit strikten Typen, Interfaces und Value Objects schreibt, denkt bereits in denselben Kategorien wie ein TypeScript-Entwickler: Welche Form hat dieser Wert, welche Zustände kann er annehmen, und was garantiert der Compiler mir davon zur Laufzeit.

Der größte Denkfehler beim Umstieg ist die Annahme, TypeScript verhalte sich wie eine strengere Version von PHP. Tatsächlich ist TypeScript ein reines Kompilierzeit-Typsystem ohne jede Laufzeitprüfung, während PHP Typdeklarationen aktiv zur Laufzeit durchsetzt und bei Verstoß eine TypeError-Exception wirft. Diese Verschiebung, von einer durchsetzenden zu einer rein beratenden Typebene, ist der rote Faden, der sich durch fast jedes Konzept in diesem Artikel zieht und in nahezu jedem Abschnitt neu auftaucht.

2. Union Types: PHP 8 vs. TypeScript und das Narrowing

PHP 8 führte Union Types wie string|int für Parameter, Properties und Rückgabewerte ein. Zur Laufzeit prüft die Engine bei jedem Funktionsaufruf, ob der übergebene Wert zu einem der Typen im Union passt, und wirft andernfalls eine TypeError. In TypeScript sieht die Syntax mit string | number fast identisch aus, doch die Prüfung findet ausschließlich beim Kompilieren statt. Zur Laufzeit existiert im transpilierten JavaScript keinerlei Union-Type-Information mehr, das Typsystem wird komplett wegoptimiert (Type Erasure).

Der praktische Unterschied zeigt sich beim Narrowing, also der Verengung eines Union-Typs auf einen konkreten Typ innerhalb eines Codepfads. In PHP übernimmt gettype(), is_string() oder instanceof diese Aufgabe, wobei PHPStan und Psalm den Typ nach einer solchen Prüfung im weiteren Codeverlauf korrekt verengen. TypeScript nutzt dafür typeof für primitive Typen, instanceof für Klassen und den in-Operator für strukturelle Prüfungen, alles direkt in der Sprache verankert statt über ein separates Statik-Analyse-Tool. Der Compiler verengt den Typ automatisch in jedem Zweig einer if-Verzweigung, ganz ohne zusätzliche Annotation.


<?php

declare(strict_types=1);

final class PriceFormatter
{
    /**
     * Formats a price that may arrive as a formatted string or raw cents.
     *
     * @param string|int $price
     */
    public function format(string|int $price): string
    {
        if (is_int($price)) {
            // Narrowed to int: PHPStan knows $price is int here
            return number_format($price / 100, 2) . ' EUR';
        }

        // Narrowed to string in the remaining branch
        return trim($price);
    }
}

// TypeScript equivalent: union type with native narrowing
function formatPrice(price: string | number): string {
  if (typeof price === 'number') {
    // Narrowed to number: TS knows price is number here
    return `${(price / 100).toFixed(2)} EUR`;
  }

  // Narrowed to string in the remaining branch, no annotation needed
  return price.trim();
}

3. Interfaces und abstrakte Klassen: Laufzeit vs. reine Kompilierzeit

PHP-Interfaces sind Laufzeit-Konstrukte: Eine Klasse, die ein Interface implementiert, kann zur Laufzeit per instanceof geprüft werden, und das Interface taucht in class_implements() auf. Abstrakte Klassen gehen weiter und können bereits konkrete Methodenimplementierungen und Konstruktorlogik mitbringen. Beide Konstrukte existieren als reale Einträge in der Klassenhierarchie, die die PHP-Engine zur Laufzeit kennt und auswerten kann.

TypeScript-Interfaces dagegen sind ein reines Kompilierzeit-Konstrukt ohne jeden Laufzeit-Fußabdruck: Sie werden beim Transpilieren zu JavaScript vollständig entfernt, es gibt kein instanceof MyInterface und keine Möglichkeit, zur Laufzeit zu prüfen, ob ein Objekt ein bestimmtes Interface erfüllt. Type Aliases (type Foo = {...}) verhalten sich ähnlich, unterscheiden sich von Interfaces aber darin, dass sie auch Unions, Intersections und primitive Typen benennen können, während Interfaces auf Objektformen und deren Erweiterung über extends beschränkt sind. Wer zur Laufzeit tatsächlich prüfen will, ob ein Objekt eine bestimmte Form hat, braucht in TypeScript eine explizite Type Guard-Funktion, die die Struktur manuell mit typeof- oder in-Prüfungen validiert.


<?php

declare(strict_types=1);

interface PriceProviderInterface
{
    public function getPrice(): int;
}

final class CatalogPrice implements PriceProviderInterface
{
    public function __construct(private readonly int $cents) {}

    public function getPrice(): int
    {
        return $this->cents;
    }
}

// Runtime check is possible in PHP
if ($object instanceof PriceProviderInterface) {
    echo $object->getPrice();
}

// TypeScript interface: pure compile-time contract, erased at runtime
interface PriceProvider {
  getPrice(): number;
}

class CatalogPrice implements PriceProvider {
  constructor(private readonly cents: number) {}

  getPrice(): number {
    return this.cents;
  }
}

// No "instanceof PriceProvider" at runtime, interfaces vanish after compilation.
// A manual type guard is required to check shape at runtime.
function isPriceProvider(value: unknown): value is PriceProvider {
  return typeof value === 'object' && value !== null && 'getPrice' in value;
}

4. Nominale vs. strukturelle Typisierung: Wo das Modell kippt

Der wohl wichtigste Unterschied zwischen den beiden Typsystemen ist die Kompatibilitätsregel. PHP nutzt nominale Typisierung: Zwei Klassen sind nur dann kompatibel, wenn eine tatsächlich von der anderen erbt oder ein gemeinsames Interface implementiert, unabhängig davon, wie ähnlich ihre Properties und Methoden aussehen. Eine Klasse Address mit den Feldern street, city, zip ist in PHP niemals kompatibel zu einer strukturell identischen Klasse ShippingAddress, solange keine explizite Vererbungsbeziehung besteht.

TypeScript nutzt dagegen strukturelle Typisierung, oft als „Duck Typing" bezeichnet: Zwei Typen sind kompatibel, sobald ihre Form übereinstimmt, unabhängig vom Namen oder einer deklarierten Beziehung. Eine Funktion, die einen Parameter vom Typ { street: string; city: string; zip: string } erwartet, akzeptiert klaglos jedes Objekt mit diesen drei Feldern, egal ob es als Address, ShippingAddress oder gar nicht typisiert deklariert wurde. Das führt zu einer überraschenden Konsequenz: Zwei völlig unabhängig entwickelte Klassen mit identischer Form sind in TypeScript austauschbar, obwohl sie fachlich nichts miteinander zu tun haben, etwa ein Coordinate-Objekt mit x/y und ein PixelOffset-Objekt mit denselben Feldnamen. PHP-Entwickler, die diese Falle nicht kennen, wundern sich, warum TypeScript einen offensichtlichen Bug nicht meldet, den PHPStan durch die nominale Typprüfung sofort gefangen hätte.

5. Readonly und Immutability: Garantien im Vergleich

PHP 8.1 führte readonly Properties ein, PHP 8.2 erweiterte das Konzept auf ganze readonly Klassen. Der entscheidende Punkt: Diese Garantie wird von der PHP-Engine zur Laufzeit durchgesetzt. Ein Zuweisungsversuch auf eine bereits initialisierte readonly Property außerhalb des Konstruktors wirft eine Error-Exception, egal ob der Aufrufer die Typprüfung umgeht oder nicht. Kombiniert mit Constructor Property Promotion lassen sich unveränderliche Value Objects in wenigen Zeilen deklarieren, ohne Getter-Boilerplate.

TypeScript kennt den Modifier readonly ebenfalls, sowohl für Klassen-Properties als auch für Array- und Tupel-Typen mit readonly T[]. Der fundamentale Unterschied: readonly in TypeScript ist eine reine Kompilierzeit-Prüfung. Nach dem Transpilieren zu JavaScript existiert keinerlei Schutz mehr, ein direkter Zugriff über JavaScript, über as any-Casts oder aus einer nicht typgeprüften Bibliothek heraus kann den vermeintlich unveränderlichen Wert klaglos überschreiben. Echte Laufzeit-Immutability erreicht man in TypeScript nur zusätzlich über Object.freeze(), das JavaScript-Feature bleibt und eben nicht durch den readonly-Modifier selbst.


<?php

declare(strict_types=1);

final readonly class Money
{
    /**
     * Immutable value object, enforced by the PHP engine at runtime.
     */
    public function __construct(
        public int $amountInCents,
        public string $currency,
    ) {}
}

$price = new Money(1999, 'EUR');
// $price->amountInCents = 2500; // throws Error at runtime, not just a lint warning

// TypeScript equivalent: readonly is compile-time only
class Money {
  constructor(
    public readonly amountInCents: number,
    public readonly currency: string,
  ) {}
}

const price = new Money(1999, 'EUR');
// price.amountInCents = 2500; // TS compile error, but works fine in plain JS at runtime
// True runtime protection requires Object.freeze(price) in addition to readonly

6. Enums: PHP 8.1 vs. Union-of-Literals in TypeScript

PHP 8.1 brachte native Enums mit optionaler Backing-Type-Unterstützung für int oder string. Ein PHP-Enum ist eine echte Klasse mit einer festen Menge von Instanzen, kann Interfaces implementieren, eigene Methoden besitzen und wird von der Engine zur Laufzeit als eigener Typ geführt. OrderStatus::Shipped ist ein Singleton-Objekt, kein bloßer String.

TypeScript besitzt zwar ein eigenes enum-Schlüsselwort, doch in der Praxis gilt eine Union aus String- oder Zahlen-Literalen als idiomatischer: type OrderStatus = 'pending' | 'shipped' | 'delivered'. Der Grund liegt in mehreren TypeScript-spezifischen Eigenheiten nativer Enums: Sie erzeugen zusätzlichen Laufzeit-Code (ein Objekt mit bidirektionalem Mapping bei numerischen Enums), verhalten sich bei const enum anders unter isolatedModules-Kompilierung, und lassen sich schlechter mit reinem JSON-Datenaustausch kombinieren. Literal-Unions dagegen sind reine Kompilierzeit-Konstrukte ohne jeden Laufzeit-Overhead und verhalten sich exakt wie gewöhnliche Strings, was sie zur bevorzugten Wahl in modernen TypeScript-Codebasen macht, insbesondere bei API-Grenzen zu JSON-basierten Backends wie einer Magento-GraphQL-Schnittstelle.


<?php

declare(strict_types=1);

enum OrderStatus: string
{
    case Pending = 'pending';
    case Shipped = 'shipped';
    case Delivered = 'delivered';

    public function isFinal(): bool
    {
        return $this === self::Delivered;
    }
}

$status = OrderStatus::Shipped;

// Idiomatic TypeScript alternative: union of string literals, zero runtime overhead
type OrderStatus = 'pending' | 'shipped' | 'delivered';

function isFinal(status: OrderStatus): boolean {
  return status === 'delivered';
}

const status: OrderStatus = 'shipped';

7. Generics: PHPStan-Templates vs. native TypeScript-Generics

PHP kennt bis heute keine nativen Generics in der Sprache selbst. Statische Analyse-Tools wie PHPStan und Psalm simulieren Generics über spezielle Docblock-Annotationen wie @template T und @return T, die zur Laufzeit von der PHP-Engine komplett ignoriert werden. Diese Annotationen existieren nur, damit das Analyse-Tool Typen konsistent durch generische Collection-Klassen wie ein Collection<Product>-Objekt hindurchreichen kann, ohne dass PHP selbst davon Kenntnis hat.

TypeScript dagegen hat Generics als natives Sprachfeature, direkt im Compiler verankert, mit derselben spitzen Klammer-Syntax wie in Java oder C#: function first<T>(items: T[]): T. Der Compiler prüft Typparameter konsistent über Funktionsgrenzen, Klassen und Interfaces hinweg, inklusive Constraints (T extends { id: number }) und Default-Typen. Für PHP-Entwickler, die mit PHPStan-Templates bereits vertraut sind, ist der Umstieg überraschend klein, das mentale Modell einer generischen Collection-Klasse ist identisch, nur dass die Prüfung nicht mehr über ein separates externes Tool läuft, sondern direkt Teil der Sprache ist und in jeder IDE ohne Zusatzkonfiguration funktioniert.

PHP-Konzept TypeScript-Entsprechung Wesentlicher Unterschied
Union Types (string|int) Unions (string | number) PHP prüft zur Laufzeit, TS nur beim Kompilieren
Interfaces Interfaces / Type Aliases TS-Interfaces existieren nach dem Build nicht mehr
Klassenidentität (instanceof) Objektform (Shape) Vorsicht: TS akzeptiert strukturell gleiche, fachlich fremde Objekte
readonly Properties readonly-Modifier PHP wirft Laufzeit-Error, TS nur Compile-Fehler
Enums (PHP 8.1) Literal-Unions oder enum Literal-Unions sind ohne Laufzeit-Overhead idiomatischer

8. Null-Safety: Nullable Types vs. strictNullChecks

PHP kennzeichnet nullable Typen mit einem vorangestellten Fragezeichen, ?string ist Kurzschreibweise für string|null. Der Null-Coalescing-Operator ?? und seit PHP 7.4 der Nullsafe-Operator ?-> (PHP 8.0) reduzieren verschachtelte isset()-Prüfungen auf eine Zeile. Ohne declare(strict_types=1) und eine konsequente Nutzung nullable Typen bleibt null in PHP jedoch ein Wert, der an praktisch jeder Stelle unbemerkt durchrutschen kann, wenn die Typprüfung nicht aktiv eingeschaltet ist.

TypeScript macht Null-Sicherheit zu einer expliziten Compiler-Option: strictNullChecks, standardmäßig Teil von strict: true. Ist die Option aktiv, ist null kein automatischer Bestandteil jedes Typs mehr, ein Wert vom Typ string kann dann garantiert niemals null sein, es sei denn, der Typ ist explizit als string | null deklariert. Optional Chaining (?.) und der Nullish-Coalescing-Operator (??) entsprechen syntaktisch fast exakt ihren PHP-Pendants. Der entscheidende Vorteil von strictNullChecks: Der Compiler zwingt an jeder Stelle, an der ein nullable Wert verwendet wird, zu einer expliziten Prüfung, bevor der Code überhaupt kompiliert, während PHP diese Disziplin nur über zusätzliche Statik-Analyse-Tools wie PHPStan auf hohem Level durchsetzt.

9. DTO- und Value-Object-Vergleich: PHP und TypeScript nebeneinander

Ein typisches Beispiel, an dem sich fast alle bisherigen Konzepte bündeln, ist ein Address Value Object, wie es in einem Checkout-Flow zwischen Magento-Backend und einem headless Frontend ausgetauscht wird. In PHP definiert man ein solches Objekt idealerweise als readonly Klasse mit Constructor Property Promotion, optional mit einem Interface für die Service-Contract-Schicht und einer fromArray()-Factory-Methode für die Deserialisierung aus einem API-Response.

Die TypeScript-Entsprechung nutzt entweder eine Klasse mit readonly-Feldern, analog zur PHP-Variante, oder, deutlich häufiger in headless Frontend-Code, ein reines type-Alias ohne Klasse und ohne Laufzeit-Objekt, kombiniert mit einer separaten Parsing-Funktion oder einer Bibliothek wie Zod für Laufzeitvalidierung an der API-Grenze. Diese Kombination aus Compile-Time-Typ plus Runtime-Validator ist der TypeScript-Ersatz für das, was PHP durch die eingebaute Typprüfung an der Funktionsgrenze automatisch bekommt, denn ein type-Alias allein garantiert zur Laufzeit gar nichts, sobald Daten von außen, etwa aus einem fetch()-Response, ins System gelangen.


<?php

declare(strict_types=1);

final readonly class Address
{
    public function __construct(
        public string $street,
        public string $city,
        public string $zip,
        public string $countryCode,
    ) {}

    /**
     * @param array{street: string, city: string, zip: string, countryCode: string} $data
     */
    public static function fromArray(array $data): self
    {
        return new self(
            $data['street'],
            $data['city'],
            $data['zip'],
            $data['countryCode'],
        );
    }
}

// TypeScript equivalent: type-only shape plus a runtime parser at the API boundary
type Address = {
  readonly street: string;
  readonly city: string;
  readonly zip: string;
  readonly countryCode: string;
};

// The "type" alone gives zero runtime guarantee for data coming from fetch();
// a parser like this (or a Zod schema) is the TS substitute for PHP's built-in check.
function parseAddress(data: unknown): Address {
  if (
    typeof data !== 'object' || data === null ||
    !('street' in data) || !('city' in data) ||
    !('zip' in data) || !('countryCode' in data)
  ) {
    throw new Error('Invalid address payload');
  }

  const raw = data as Record<string, unknown>;
  return {
    street: String(raw.street),
    city: String(raw.city),
    zip: String(raw.zip),
    countryCode: String(raw.countryCode),
  };
}

Mironsoft

Magento-Backend und TypeScript-Frontend aus einer Hand

TypeScript- und Magento-Expertise verbinden?

Wir entwickeln Magento-Backends und headless TypeScript-Frontends mit sauber typisierten Service Contracts, konsistenten DTOs zwischen PHP und TypeScript und pragmatischen Build- und Tooling-Setups für euer Team.

Typsystem-Audit

PHPStan- und TypeScript-Konfiguration auf strikte Konsistenz prüfen

DTO-Synchronisation

Value Objects zwischen Magento-API und TypeScript-Frontend abgleichen

Team-Schulung

Praxisworkshop für PHP-Entwickler auf dem Weg zu TypeScript

10. Zusammenfassung

Für PHP-Entwickler ist TypeScript keine fremde Welt, sondern eine Verschiebung vertrauter Konzepte auf eine andere Durchsetzungsebene. Union Types, Interfaces, Readonly-Properties und Enums existieren in beiden Sprachen, doch TypeScript prüft sie ausschließlich beim Kompilieren und verwirft jede Typinformation danach vollständig, während PHP dieselben Garantien aktiv zur Laufzeit durchsetzt. Wer diesen Unterschied verinnerlicht, vermeidet die häufigsten Anfängerfehler: sich auf einen type-Alias als Laufzeit-Validierung zu verlassen oder ein readonly-Feld für unveränderlich zu halten, obwohl es nach dem Build ungeschützt ist.

Der zweite entscheidende Kippunkt ist der Wechsel von nominaler zu struktureller Typisierung. Während PHP zwei Klassen nur über eine explizite Vererbungs- oder Interface-Beziehung kompatibel macht, genügt TypeScript die reine Übereinstimmung der Form. Diese Eigenschaft ist mächtig für flexible API-Grenzen, kann aber echte Bugs verschleiern, wenn fachlich unterschiedliche Objekte zufällig dieselbe Struktur besitzen. Mit diesem Wissen im Hinterkopf lässt sich ein DTO wie das Address-Beispiel aus Abschnitt 9 in beiden Sprachen konsistent und typsicher modellieren.

TypeScript für PHP-Entwickler - Das Wichtigste auf einen Blick

Union Types

PHP prüft string|int zur Laufzeit, TypeScript nur beim Kompilieren. Narrowing per typeof/instanceof statt gettype().

Interfaces

TS-Interfaces verschwinden nach dem Build vollständig, kein instanceof-Check zur Laufzeit möglich.

Nominal vs. Structural

PHP prüft Klassenidentität, TypeScript nur die Objektform. Quelle für überraschende Kompatibilität.

Readonly & Enums

PHP wirft Laufzeit-Errors, TS nur Compile-Fehler. Literal-Unions statt enum in TS bevorzugen.

11. FAQ: TypeScript für PHP-Entwickler

1Ist TypeScript für erfahrene PHP-Entwickler schwer zu lernen?
Nein, die meisten Konzepte wie Union Types, Interfaces und Generics kennen PHP-Entwickler bereits aus PHP 8 und PHPStan. Die größte Umstellung ist das rein kompilierzeit-basierte Typsystem ohne Laufzeitprüfung.
2Werden TypeScript-Typen zur Laufzeit geprüft wie in PHP?
Nein. TypeScript-Typen existieren nur beim Kompilieren und werden beim Transpilieren vollständig entfernt. PHP prüft deklarierte Typen aktiv zur Laufzeit und wirft bei Verstoß eine TypeError.
3Was ist der Unterschied zwischen nominaler und struktureller Typisierung?
PHP: Klassen nur über explizite Vererbung/Interfaces kompatibel. TypeScript: Kompatibilität allein über die Objektform, unabhängig vom Namen.
4Sind TypeScript-Interfaces mit PHP-Interfaces vergleichbar?
Syntaktisch ja, funktional nur eingeschränkt. PHP-Interfaces sind Laufzeit-Konstrukte, TypeScript-Interfaces verschwinden nach dem Build vollständig.
5Ist ein readonly-Feld in TypeScript wirklich unveränderlich?
Nur zur Kompilierzeit. Nach dem Transpilieren existiert kein Schutz mehr. Echte Laufzeit-Immutability erfordert zusätzlich Object.freeze().
6Soll ich in TypeScript native Enums oder Literal-Unions verwenden?
Literal-Unions gelten als idiomatischer, da sie keinen Laufzeit-Overhead erzeugen und sich besser mit JSON-Datenaustausch vertragen.
7Wie verhalten sich TypeScript-Generics im Vergleich zu PHPStan-Templates?
PHPStan simuliert Generics über @template-Docblocks, die PHP zur Laufzeit ignoriert. TypeScript hat Generics nativ im Compiler, mit Java/C#-ähnlicher Syntax.
8Was entspricht PHPs Nullsafe-Operator in TypeScript?
Optional Chaining (?.) entspricht syntaktisch fast exakt PHPs ?->-Operator. Für echte Null-Sicherheit zusätzlich strictNullChecks aktivieren.
9Reicht ein TypeScript-type-Alias als Ersatz für PHP-Typprüfung bei API-Daten?
Nein. Ein type-Alias garantiert zur Laufzeit nichts bei externen Daten. Dafür braucht es eine Parsing-Funktion oder eine Validierungsbibliothek wie Zod.
10Kann strukturelle Typisierung in TypeScript echte Bugs verschleiern?
Ja. Zwei fachlich unterschiedliche Objekte mit identischer Feldstruktur sind in TypeScript kompatibel, obwohl PHPStan dank nominaler Typisierung sofort warnen würde.