flexibel und typsicher in React und TypeScript
Polymorphe Komponenten lassen den Aufrufer entscheiden, welches HTML Element tatsaechlich gerendert wird, ohne dass die Komponente selbst dafuer eine eigene Variante braucht. Mit dem as Prop und ElementType aus TypeScript bleibt diese Flexibilitaet vollstaendig typsicher, inklusive korrekt vererbter Props und funktionierendem Ref Forwarding.
Inhaltsverzeichnis
- 1. Was polymorphe Komponenten wirklich loesen
- 2. Das as Prop Grundprinzip
- 3. Typisierung mit ElementType
- 4. Props Vererbung mit ComponentPropsWithoutRef
- 5. Ref Forwarding bei polymorphen Komponenten
- 6. Eigene Polymorphic Component Factory bauen
- 7. Barrierefreiheit bei wechselnden Elementen
- 8. Fallstricke und Performance
- 9. as Prop im Vergleich zu Alternativen
- 10. Zusammenfassung
- 11. FAQ
1. Was polymorphe Komponenten wirklich loesen
Eine polymorphe Komponente rendert je nach uebergebenem Prop ein anderes HTML Element oder eine andere Komponente, ohne dass sich das aeussere Verhalten oder die Props dabei aendern. Der klassische Fall: Ein Button soll manchmal als <button>, manchmal als <a> und manchmal als React Router Link gerendert werden, aber immer identisch aussehen und sich identisch verhalten. Ohne polymorphe Komponenten muesste man drei separate Komponenten pflegen, die sich nur im Root Element unterscheiden.
Design Systeme stossen dieses Problem besonders haeufig, weil Buttons, Text und Layout Container aus semantischen Gruenden unterschiedliche Elemente brauchen. Eine Ueberschrift soll wahlweise als h1 bis h6 gerendert werden, ein Text Baustein manchmal als span, manchmal als p, manchmal als label. Polymorphe Komponenten mit dem as Prop loesen genau dieses Problem: eine einzige Implementierung, ein einziger Satz an visuellen Props, aber freie Wahl des tatsaechlichen DOM Elements durch den Aufrufer.
2. Das as Prop Grundprinzip
Das Grundprinzip einer polymorphen Komponente ist simpel: Ein Prop namens as nimmt einen Elementnamen oder eine Komponente entgegen, und die Implementierung rendert dynamisch genau dieses Element. In purem JavaScript sieht das so aus, dass man den Wert von as in einer Variablen mit Grossbuchstaben speichert, weil JSX Grossbuchstaben als Komponentenreferenz und Kleinbuchstaben als natives HTML Element interpretiert. Diese Variable wird dann anstelle eines festen Tag Namens im JSX verwendet.
Der eigentliche Wert von polymorphen Komponenten zeigt sich erst, wenn zusaetzlich alle Standard Props des jeweiligen Elements korrekt durchgereicht werden. Ein als as="a" gerenderter Button muss href akzeptieren, waehrend derselbe Button als as="button" stattdessen type="submit" akzeptieren sollte. Ohne saubere Typisierung verliert man hier entweder Typsicherheit komplett oder muss jede Kombination manuell absichern, was bei wachsender Anzahl unterstuetzter Elemente schnell unhandlich wird.
// Text.jsx — minimal polymorphic component, plain JavaScript version
function Text({ as: Component = "span", className, children, ...rest }) {
return (
<Component className={className} {...rest}>
{children}
</Component>
);
}
// Usage — same visual component, different rendered elements
// <Text>Default span</Text>
// <Text as="p" className="lead">Rendered as a paragraph</Text>
// <Text as="label" htmlFor="email">Rendered as a label</Text>
// <Text as={Link} to="/pricing">Rendered as a router Link</Text>
3. Typisierung mit ElementType
In TypeScript beginnt eine typsichere polymorphe Komponente mit dem eingebauten Typ ElementType. Er akzeptiert sowohl String Literale wie "button" oder "div" als auch React Komponenten Referenzen. Der generische Parameter der Komponente wird an ElementType gebunden, sodass TypeScript bei jedem Aufruf weiss, welches konkrete Element gerade aktiv ist, und die passenden Props dafuer einfordert.
Ohne ElementType muesste man entweder auf string zurueckgreifen, was jede Typsicherheit fuer die Props aufgibt, oder fuer jedes unterstuetzte Element eine eigene Overload Signatur schreiben, was bei polymorphen Komponenten mit vielen moeglichen Elementen schnell unwartbar wird. ElementType gibt genau die richtige Balance: generisch genug fuer beliebige Elemente, aber praezise genug, um bei as="button" automatisch onClick mit dem richtigen Event Typ anzubieten.
// polymorphic-types.ts — the ElementType foundation
import type { ElementType, ComponentPropsWithoutRef } from "react";
// Generic constraint: C must be a valid tag name or component
type PolymorphicProps<C extends ElementType, Props = object> = Props & {
as?: C;
} & Omit<ComponentPropsWithoutRef<C>, keyof Props | "as">;
// Example: a Box component with its own props plus the target element's props
type BoxOwnProps = {
padding?: "sm" | "md" | "lg";
};
function Box<C extends ElementType = "div">({
as,
padding = "md",
className,
...rest
}: PolymorphicProps<C, BoxOwnProps>) {
const Component = as || "div";
return <Component className={`box box-${padding} ${className ?? ""}`} {...rest} />;
}
// TypeScript now knows the exact prop shape per "as" value:
// <Box as="a" href="/docs"> -> href is required/typed for anchors
// <Box as="button" onClick={...} /> -> onClick typed as button event
4. Props Vererbung mit ComponentPropsWithoutRef
Damit eine polymorphe Komponente wirklich als Drop in Replacement fuer das native Element funktioniert, muessen alle Standard Props des Zielelements automatisch verfuegbar sein, ohne sie manuell aufzulisten. ComponentPropsWithoutRef<C> extrahiert genau diese Props aus dem generischen Parameter C, egal ob es sich um ein natives Element oder eine eigene Komponente handelt. Das WithoutRef im Namen ist wichtig, weil ref bei polymorphen Komponenten separat behandelt werden muss.
Der Trick bei der Kombination aus eigenen Props und geerbten Props liegt im Omit: Eigene Props wie padding im vorherigen Beispiel sollen Vorrang vor gleichnamigen nativen Props haben, falls es Ueberschneidungen gibt. Ohne dieses Omit wuerde TypeScript bei Namenskollisionen einen Union Typ bilden, der in der Praxis oft nicht das gewuenschte Verhalten fuer die polymorphe Komponente ergibt. Diese Reihenfolge, eigene Props zuerst definieren und dann native Props mit Ausnahme der eigenen Namen hinzufuegen, ist der zuverlaessigste Ansatz.
5. Ref Forwarding bei polymorphen Komponenten
Ref Forwarding ist der Teil, der bei polymorphen Komponenten am haeufigsten falsch oder unvollstaendig implementiert wird. Da das tatsaechliche Zielelement erst zur Laufzeit feststeht, muss der Ref Typ ebenfalls generisch sein: Ein as="a" Aufruf braucht einen HTMLAnchorElement Ref, ein as="button" Aufruf einen HTMLButtonElement Ref. Der Hilfstyp ElementRef<C> aus React loest genau das, indem er den korrekten Ref Typ aus dem generischen Element Parameter ableitet.
Seit React 19 ist ref ein normales Prop und muss nicht mehr zwingend ueber forwardRef weitergereicht werden, was den Code fuer polymorphe Komponenten spuerbar vereinfacht. In aelteren Codebasen mit React 18 oder frueher bleibt forwardRef notwendig, kombiniert mit einem expliziten generischen Typ Parameter, weil forwardRef selbst generische Komponenten nur eingeschraenkt unterstuetzt und man haeufig eine kleine Typ Assertion braucht, um beide Generics sauber durchzureichen.
// Button.tsx — polymorphic component with correctly typed ref (React 19)
import type { ElementType, ElementRef, ComponentPropsWithoutRef } from "react";
type ButtonOwnProps = {
variant?: "primary" | "ghost";
};
type ButtonProps<C extends ElementType> = ButtonOwnProps & {
as?: C;
ref?: ElementRef<C> | null;
} & Omit<ComponentPropsWithoutRef<C>, keyof ButtonOwnProps | "as" | "ref">;
function Button<C extends ElementType = "button">({
as,
variant = "primary",
ref,
className,
...rest
}: ButtonProps<C>) {
const Component = as || "button";
return (
<Component ref={ref} className={`btn btn-${variant} ${className ?? ""}`} {...rest} />
);
}
// Usage — ref type adapts automatically to the "as" value
// const anchorRef = useRef<HTMLAnchorElement>(null);
// <Button as="a" href="/docs" ref={anchorRef}>Docs</Button>
6. Eigene Polymorphic Component Factory bauen
Sobald ein Design System mehr als zwei oder drei polymorphe Komponenten braucht, lohnt sich eine gemeinsame Factory Funktion, die den generischen Typ und die Ref Weiterleitung an einer einzigen Stelle definiert. Statt jede Komponente einzeln mit der vollen generischen Signatur zu schreiben, kapselt eine Hilfsfunktion createPolymorphicComponent das Boilerplate und liefert eine fertige, typisierte Komponente zurueck. Das reduziert Wiederholung erheblich und stellt sicher, dass alle polymorphen Bausteine desselben Design Systems sich identisch verhalten.
Eine solche Factory nimmt typischerweise eine Render Funktion entgegen, die das Standardelement plus die eigenen Props kennt, und gibt eine Komponente zurueck, die bereits vollstaendig fuer beliebige as Werte generisch typisiert ist. Dieser Ansatz zahlt sich vor allem in Bibliotheken mit zehn oder mehr polymorphen Komponenten aus, wo die manuelle Wiederholung der generischen Signatur sonst zu einer erheblichen Fehlerquelle bei Refactorings wuerde.
7. Barrierefreiheit bei wechselnden Elementen
Ein oft uebersehener Aspekt polymorpher Komponenten ist, dass ein Wechsel des Root Elements automatisch die Semantik fuer Screenreader und Tastaturnutzer aendert. Ein <button> ist per Definition fokussierbar und per Enter oder Leertaste ausloesbar, waehrend ein <div> das nicht ist. Wird eine Button Komponente mit as="div" gerendert, ohne zusaetzliche ARIA Attribute und Tastatur Handler zu ergaenzen, entsteht ein Element, das visuell wie ein Button aussieht, sich fuer Screenreader Nutzer aber wie ein bedeutungsloser Container verhaelt.
Die zuverlaessigste Absicherung ist, das as Prop bei interaktiven polymorphen Komponenten auf eine sinnvolle Teilmenge einzuschraenken, etwa nur "button", "a" und explizit unterstuetzte Link Komponenten, statt beliebige Elemente zuzulassen. Zusaetzlich sollte die Komponente bei nicht nativ interaktiven Zielelementen automatisch role="button", tabIndex={0} und einen Tastatur Handler fuer Enter und Leertaste ergaenzen, damit die Barrierefreiheit unabhaengig vom gewaehlten Element erhalten bleibt.
8. Fallstricke und Performance
Ein haeufiger Fehler bei polymorphen Komponenten ist, die Zielkomponente bei jedem Render neu zu berechnen, ohne dabei auf Referenzstabilitaet zu achten. Da Component in der JSX Ausdruck als Variable verwendet wird, fuehrt eine instabile Referenz zwischen Renders dazu, dass React das komplette Element inklusive aller Kindkomponenten neu mountet, statt es nur zu aktualisieren. Das zerstoert lokalen State in Kindkomponenten und kann bei Formularen oder Animationen sichtbare Fehler verursachen.
Ein zweiter Fallstrick betrifft komplexe generische Signaturen: Sehr tief verschachtelte polymorphe Komponenten mit mehreren generischen Ebenen koennen die Typinferenz von TypeScript so stark belasten, dass Editor Autovervollstaendigung spuerbar langsamer wird. In der Praxis reicht meist ein einziger generischer Parameter fuer as, zusaetzliche Generics fuer andere Aspekte der Komponente sollten nur eingefuehrt werden, wenn sie wirklich noetig sind, nicht aus reinem Voraus Design.
9. as Prop im Vergleich zu Alternativen
Der as Prop ist nicht die einzige Loesung fuer polymorphe Komponenten. Die folgende Tabelle vergleicht die gaengigen Ansaetze.
| Ansatz | Typsicherheit | Flexibilitaet | Komplexitaet |
|---|---|---|---|
| as Prop mit ElementType | Hoch, mit Aufwand | Beliebiges Element zur Laufzeit | Mittel bis hoch |
| asChild / Slot Pattern | Hoch | Ein einziges Kindelement ersetzt Root | Mittel |
| Separate Varianten Komponenten | Sehr hoch | Gering, feste Auswahl | Gering pro Komponente, hoch in Summe |
| any / string Typ fuer as | Keine | Voll | Sehr gering |
In der Praxis ist der as Prop mit ElementType der beste Kompromiss fuer generische Design System Bausteine wie Text, Box oder Button, waehrend das Slot Pattern sich besser eignet, wenn genau ein Kindelement das Root ersetzen soll, etwa bei einem Link, der optisch wie ein Button aussehen soll. Separate Varianten Komponenten lohnen sich nur, wenn die Anzahl der moeglichen Elemente klein und stabil ist und maximale Typsicherheit ohne generische Komplexitaet gewuenscht ist.
Mironsoft
React Component Design und TypeScript
Typsichere polymorphe Komponenten fuer euer Design System?
Wir entwerfen und implementieren polymorphe Bausteine mit sauberer TypeScript Typisierung, korrektem Ref Forwarding und barrierefreiem Verhalten unabhaengig vom gewaehlten Element.
Typ Architektur
ElementType und generische Signaturen fuer euer Component API
Component Factory
Wiederverwendbares Boilerplate fuer alle polymorphen Bausteine
Barrierefreiheits-Check
ARIA und Tastatursteuerung unabhaengig vom gerenderten Element
10. Zusammenfassung
Polymorphe Komponenten mit dem as Prop loesen ein zentrales Design System Problem: dieselbe Komponente soll unterschiedliche HTML Elemente rendern koennen, ohne dass Typsicherheit oder Barrierefreiheit verloren gehen. ElementType bindet das gewaehlte Element generisch an die Komponente, ComponentPropsWithoutRef vererbt automatisch alle passenden Standard Props, und ElementRef sorgt fuer einen korrekt typisierten Ref, egal welches Element aktiv ist.
Wer polymorphe Komponenten in einer Bibliothek mit vielen Bausteinen einsetzt, profitiert von einer gemeinsamen Factory Funktion, die das generische Boilerplate an einer Stelle buendelt. Wichtig bleibt in jedem Fall, das as Prop bei interaktiven Komponenten auf sinnvolle Werte einzuschraenken und bei nicht nativ interaktiven Zielelementen ARIA Attribute sowie Tastatur Handler zu ergaenzen, damit die Flexibilitaet nicht auf Kosten der Barrierefreiheit geht.
Polymorphe Komponenten mit dem as Prop — Das Wichtigste auf einen Blick
Grundprinzip
Ein as Prop bestimmt das gerenderte Element, die visuelle Logik bleibt fuer alle Varianten identisch.
Typisierung
ElementType plus ComponentPropsWithoutRef vererbt automatisch die richtigen Props je Element.
Ref Forwarding
ElementRef leitet den korrekten Ref Typ ab. Seit React 19 ist ref ein normales Prop.
Barrierefreiheit
as Werte einschraenken, ARIA und Tastatur Handler bei nicht nativ interaktiven Elementen ergaenzen.