Blazor mit TypeScript: Integration & Best Practices

Blazor und TypeScript gemeinsam einsetzen
08/2026
Mark N. | Solution Engineer

TL;DR

Blazor ermöglicht C# im Browser, doch für DOM-nahe Funktionen bleibt JavaScript nötig. Durch die Integration von TypeScript in Blazor-Projekte entsteht typsichere JS Interop mit besserer Wartbarkeit. Mit .NET 10, npm, tsconfig und MSBuild lässt sich eine strukturierte TypeScript-Umgebung aufbauen, inklusive Konstruktor-Interop und sauberem Disposal-Pattern.

Wie TypeScript mit frühzeitiger Fehlererkennung und Typsicherheit die Entwicklungszeit für C#-Entwickler erheblich reduzieren kann

Interoperabilität mit Blazor und JavaScript

Blazor war die Speerspitze der Web-Revolution, da es C#-Entwicklern Frameworks bereitstellt, um ihr bereits robustes Wissen und das riesige .NET-Bibliotheks-Ökosystem in den Browser zu übertragen - sei es durch eine Server-App oder WebAssembly. Dies ermöglicht die Entwicklung interaktiver Web-UIs mit C#. Bestimmte Anwendungsfälle lassen sich jedoch nicht umsetzen, sodass weiterhin auf JavaScript zurückgegriffen werden muss.

Blazor hat diese Einschränkung berücksichtigt und bietet Entwicklern eine umfangreiche API für JavaScript-Interoperabilität (im Folgenden JS Interop), die die Interaktion mit dem DOM auf vielfältige Weise ermöglicht - selbst das Aufrufen von C#-Methoden direkt aus dem DOM heraus.

Allerdings ist JavaScript nicht C#. Es kennt keine statische Typisierung und keine Typsicherheit. Fehler werden häufig erst zur Laufzeit sichtbar, was zusätzlichen Debugging-Aufwand verursacht. Genau hier setzt TypeScript an. Persönlich arbeite ich aus den genannten Gründen nicht gerne mit JS, da es meine Arbeitseffizienz erheblich verringert. Stundenlanges Debuggen von Problemen, die zur Kompilierzeit oder sogar durch Echtzeit-Codeanalyse hätten erkannt werden können. Wenn C# doch nur nativ im Browser laufen könnte…

TypeScript bietet Typsicherheit, statische Typisierung und sorgt dank frühzeitiger Fehlererkennung dafür, dass Fehler bereits während der Kompilierung erkannt werden. Obwohl es eine Obermenge von JavaScript ist, fügt es eine zusätzliche Ebene an Komfort und Sicherheit hinzu, die vielen C#-Entwicklern vertraut ist – beispielsweise durch die Arbeit mit Interfaces.

Mit dem Release von .NET 10 erschien ein neues Feature in den Änderungsprotokollen für Blazor: „Erstellen einer Instanz eines JS-Objekts mithilfe einer Konstruktorfunktion.“ Dies eröffnet neue Möglichkeiten, das Potenzial von TypeScript über das bloße Schreiben typsicherer Funktionen und die Nutzung von Modulisolation hinaus auszuschöpfen.

Der vollständige in diesem Artikel erwähnte Quellcode befindet sich in diesem GitHub-Repository.

Voraussetzungen

Um loszulegen, es wird das Folgendes benötigt:

·        Installiertes .NET 10 SDK

·        Eine Lösung mit einem Blazor Web App-Projekt

·        Grundkenntnisse in JS Interop

·        Auf dem lokalen Rechner installierte Version von Node.js

Soultioneinricthung

Installation von Microsoft.TypeScript.MSBuild und Konfiguration von tsconfig.json

TypeScript funktioniert nicht ohne weiteres mit Blazor oder JS Interop. Glücklicherweise bietet Microsoft ein NuGet-Paket namens Microsoft.TypeScript.MSBuild, das den TypeScript-Compiler auslöst und alle .ts-Dateien erkennt.

 Obwohl das NuGet-Paket eine sofort einsatzbereite Kompilierung von .ts-Dateien bietet, ist die Erstellung einer personalisierten und anpassbaren Konfigurationsstruktur wichtig, wenn man sie in Unternehmensprojekten einsetzt.

 

Im Stammverzeichnis der Lösung habe ich folgende tsconfig.base.json erstellt:

1{
2  "compilerOptions": {
3    "target": "ESNext",
4    "module": "ESNext",
5    "esModuleInterop": true,
6    "forceConsistentCasingInFileNames": true,
7    "sourceMap": true,
8    "strict": true,
9    "skipLibCheck": true,
10    "moduleResolution": "bundler"
11  },
12  "exclude": [
13    "node_modules"
14    ]
15}

 

Warum? Jedes Projekt, das TypeScript enthält, benötigt eine tsconfig.json-Datei. Mit der Basis-Konfiguration lässt sich die Wiederholung von Standardeinstellungen, die für alle Instanzen gelten sollten, erheblich reduzieren. Das NuGet-Paket kann zwar auch über Attribute in der .csproj-Datei konfiguriert werden, aber dieser Ansatz ist wie später gezeigt wird flexibler.

 

Wir können diese Basis-Konfiguration nutzen, indem wir im Verzeichnis des TypeScript-Projekts eine tsconfig.json-Datei erstellen:

{
  "extends": "../../tsconfig.base.json",
}

Hinweis: Der Pfad kann je nach Tiefe der Ordnerstruktur variieren und von diesem Beispiel abweichen.

 

Nun haben wir unsere Basis-Konfiguration und die Möglichkeit, die Grundeinstellungen auf Modul- oder Projektebene zu erweitern oder sogar zu überschreiben.

Einrichtung von npm

Auch wenn es auf den ersten Blick kontraintuitiv erscheinen mag, ermöglicht dieser Ansatz langfristig den Import etablierter Module und Deklarationsdateien sowie bei Bedarf sogar das Bundling von Paketen. Zusätzlich kann npm mehrere Workspaces erstellen, wodurch im Wesentlichen eine ähnliche hierarchische Struktur auf Modulbasis entsteht wie bei der tsconfig.json.

 

Hierfür kann eine package.json entweder über den Befehl npm init -y erstellt oder manuell wie folgt angelegt werden:

1{
2  "name": "typescriptinteropdemo",
3  "version": "1.0.0",
4  "type": "module",
5  "private": true,
6  "workspaces": [
7    // add your package.json project names here!
8  ],
9  "scripts": {
10    "build": "npm -ws run build",
11    "clean": "npm -ws run clean"
12  },
13  "license": "ISC",
14  "devDependencies": {
15    "@microsoft/dotnet-js-interop": "^10.0.0",
16    "esbuild": "^0.25.10"
17  }
18}

Dies ermöglicht uns ein zentrales Paketmanagement, von dem jedes Projekt abhängt. In unserem Fall sind das der esbuild-Bundler und die Deklarationsdatei für die JS-Interop, um Komplikationen vorzubeugen.

 

Im Wesentlichen konfigurieren wir npm strikt so, dass diese App niemals (durch npm) veröffentlicht wird und dass sie auf Modulbasis arbeitet, um die für Blazor erforderliche Modulisolation nach ECMA 2016 einzuhalten.

 

Zwei Skriptbefehle wurden hinzugefügt, die als übergeordnete Befehle dienen, um für alle Workspaces ausgelöst zu werden. Falls Sie nicht vorhaben, esbuild zu verwenden, kann dieser Schritt vollständig übersprungen werden.

 

Die Motivation hinter der npm-Konfiguration ist der Import von Deklarationsdatei-Modulen für statische Typisierung. Wenn der TypeScript-Compiler nicht weiß, womit er es zu tun hat, wird er nicht kompilieren (da wir den strikten Modus verwenden, ist dies wichtig).

 

Projekte benötigen ebenfalls eine konfigurierte package.json, um wie beabsichtigt zu funktionieren:

1{
2  "name": "typescriptinteropdemo.client", // this name goes to the workspace array in package.json
3  "version": "1.0.0",
4  "type": "module",
5  "private": true,
6  "license": "ISC"
7}

Mehr ist nicht erforderlich.

 

Libman CLI

Eine lobende Erwähnung verdient der Library Manager, ein leichtgewichtiges Tool zum Erwerb von Bibliotheken. Es ruft Bibliotheken oder Frameworks aus bekannten Content Delivery Networks (CDN) ab und speichert sie am geeigneten Ort. Dies kann sehr nützlich sein, um statische Assets projektbezogen einzubetten und die Verwendung von Bundlern und Modulimporten über npm zu vermeiden. How to install Library Manager Libman.

Anmerkungen

Da wir TypeScript-Dateien kompilieren, ist es erwähnenswert, dass bei der Nutzung von Versionskontrollsoftware (wie Git) die .map-Dateien und die kompilierten .js-Dateien ignoriert werden sollten, um doppelten Code im Repository zu vermeiden.

 

Zudem sollte betont werden, dass dieses TypeScript-Setup eine subjektive Herangehensweise darstellt, die sich in meiner Erfahrung als zuverlässig erwiesen hat, wenn Skalierbarkeit für ein Projekt relevant wird und potenzielle technische Schulden vermieden werden sollen.

Verwendung von TypeScript

Mit einer vorbereiteten Lösung, die kompilierfähig ist, müssen wir nur noch eine TypeScript-Datei erstellen und unseren relevanten Code hinzufügen:

Die Organisation von TypeScript-Dateien folgt denselben Prinzipien wie die JavaScript-Interoperabilität in Blazor. Theoretisch führen wir eine Zwischeninstanz ein, die Typregeln explizit durchsetzt (tsconfig.json und TypeScript selbst) und den Code in JS-Dateien kompiliert. Obwohl wir technisch gesehen immer noch JavaScript verwenden (da TypeScript eine Obermenge davon ist), müssen wir uns nicht direkt damit auseinandersetzen und erhalten gleichzeitig Typsicherheit und Komfort für die Entwickler.

 

Zur Veranschaulichung erstellen wir eine isolierte Datei (Component1.razor.ts), die eine TypeScript-Klasse enthält:

1import {DotNet} from '@microsoft/dotnet-js-interop'
2
3export class Component1 {
4    private readonly refObject: DotNet.DotNetObject
5
6    constructor(refObject: DotNet.DotNetObject) {
7        this.refObject = refObject
8    }
9
10    async showPrompt(value: string) {
11        alert("This message has been brought to you by the typescript class: " + value)
12        return await this.updateContent()
13    }
14    
15    async updateContent() : Promise<void> {
16        return await this.refObject.invokeMethodAsync("UpdateContent", 
17            "This message has been brought to you by the typescript class: " + new Date().toLocaleTimeString())
18    }
19}

Der Konstruktor nimmt ein .NET-Referenzobjekt entgegen, das in einer schreibgeschützten Variable gespeichert werden kann. Diese ist anschließend für alle Funktionen und Ausdrücke innerhalb der Klasse zugänglich.

 

Mithilfe der Modulisolation initialisieren wir die Klasse mit der neuen Methode aus der Interop-Schnittstelle:

1protected override async Task OnAfterRenderAsync(bool firstRender)
2{
3    if (firstRender)
4    {
5        _objRef = DotNetObjectReference.Create(this);
6        // await using to dispose of the module instance immediately, as we are only interested in the class instance
7        await using var module = await JsRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/TypeScriptInteropDemo.Rcl/Component1.razor.js");
8        _classInstance = await module.InvokeConstructorAsync("Component1", _objRef);
9    }
10}

Indem wir die Objektreferenz dieser Komponente übergeben, können wir .NET-Methoden aufrufen, die mit dem JsInvokableAttribute gekennzeichnet sind und dies direkt aus der Klasse heraus:

1[JSInvokable]
2public void UpdateContent(string content)
3{
4    _content = content;
5    StateHasChanged();
6}

Dies ergibt eine wiederverwendbare Instanz ohne merkwürdige und komplexe Logik, um isoliertes Verhalten aus einer JS/TS-Datei zu erhalten, und begrenzt den Nutzungsumfang wirklich auf das, was wir benötigen, ohne die Nuancen einer dynamisch typisierten Sprache.

 

Da wir eine JS-Objektinstanz erstellen, müssen wir auch sicherstellen, dass wir das IAsyncDisposable-Muster implementieren und die Klasseninstanz freigeben, sobald die Komponente nicht mehr verwendet wird, um Speicherlecks oder potenzielle Überläufe zu vermeiden:

1private IJSObjectReference? _classInstance;
2private DotNetObjectReference<Component1>? _objRef;
3
4public async ValueTask DisposeAsync()
5{
6    if (_classInstance is not null)
7        await _classInstance.DisposeAsync();
8
9    _objRef?.Dispose();
10}

Auch der Disposal den .NET-Objekt-Referenz ist erforderlich.

Es ist wichtig anzumerken, dass dieses Setup nicht nur für Komponenten gilt, sondern für jede C#-Datei, die Dependency Injection innerhalb der Blazor-App verwendet. Dies eröffnet völlig neue Gestaltungsmöglichkeiten.

 

Allerdings muss beachtet werden, dass dieselben Prinzipien auch für serverseitige Anwendungen gelten: Services, Handler etc., die IJSInterop verwenden, dürfen erst aufgerufen werden, nachdem die Komponente gerendert wurde.

Version Control

Der Compiler generiert nun .map-Dateien und .js-Artefaktdateien für die Verwendung in der App. Diese sollten nicht in die Versionskontrolle (in meinem Beispiel Git) aufgenommen werden, da sie die Versionierung und Codebasis nur unnötig belasten würden.

 

Möglicherweise existieren bereits JS-Dateien ohne TypeScript-Unterstützung im Projekt, oder es gibt Legacy-Code bzw. bereits vorhandene benutzerdefinierte Bibliotheken in der Lösung.

 

Ein möglicher Ansatz, dieses Problem zu umgehen, ist die Einrichtung dedizierter Ordner im wwwroot-Verzeichnis, die benötigte JS-Dateien von Artefakten trennen. Falls eine einzelne Datei nicht in dieses Schema passt, kann sie auch individuell in der .gitignore-Datei ausgeschlossen werden:

# Ignoring our build artifacts from TypeScript compiled files
**/*.js.map
**/*.js

# in order to also have ability to commit JavaScript files, a dedicated 
# folder structure is used to include those, such as lets say bundles 
!**/wwwroot/**/scripts/js/**/*.js
!**/wwwroot/**/lib/**/js/*.js


# Ignoring very niche cases would be the best practice, in case a component really needs something in JavaScropt,
# such as the ReconnectModal.razor.js
!ReconnectModal.razor.js 

Je nach Lösung kann dies für individuelle Anforderungen angepasst werden und dient als Ausgangsbeispiel für dieses Repository. Wichtig ist, dass Artefakte nicht in die Codebasis aufgenommen werden, wenn Commits durchgeführt werden.

Syntax highlighting

Durch unser NPM-Projektsetup ist nun die Syntaxhervorhebung in der IDE verfügbar:

Fazit

In diesem Artikel habe ich eine individuelle Einrichtung demonstriert, um TypeScript in einem Blazor-Projekt zu kompilieren – inklusive zusätzlicher Optionen für Paketverwaltung und Bibliotheksmanagement für die Verwendung in diesen Dateien sowie der Einrichtung einer Komponente zur Nutzung von Klassenkonstruktoren. Die wichtigste Erkenntnis ist, dass das ASP.NET Core-Team hinter Blazor stets bestrebt ist, das bestehende Ökosystem zu innovieren und zu erweitern, um die Entwicklungserfahrung zu verbessern und mehr Funktionalität bereitzustellen – bei gleichzeitiger Wahrung der Performance und einer symbiotischen Beziehung zwischen DOM und Laufzeitumgebung.

 

Dieser Ansatz reduziert deutlich den Stress, der durch Code ohne statische Typisierung entsteht. Es ist jedoch erwähnenswert, dass jedes Werkzeug seine eigenen Tücken hat, die bei unsachgemäßer Handhabung Probleme verursachen können.

Foto von Mark
Mark N. | Solution Engineer

Mehr zum Thema

Pfeil nach rechts (Verlinkung)
.NET-Ökosystem erklärt: Plattform, SDK, Core, C#, NuGet, Azure und DevOps kompakt und verständlich im Überblick für Entwicklerinnen und Entwickler.
07/2026

.NET: Framework, Plattform, Core? Was steckt eigentlich hinter dem Microsoft-Ökosystem?

Blauer Pfeil nach rechts (Verlinkung)
Aspire vereinfacht das lokale Setup verteilter .NET-Anwendungen: Orchestrierung, Service Discovery und Observability als versionierbarer C#-Code.
07/2026

Aspire: Schnellere Entwicklung, einfacherer Start

Blauer Pfeil nach rechts (Verlinkung)
Erfahren Sie, wie SQL Indizes Datenbankabfragen beschleunigen, Fallstricke vermeiden und die Performance Ihrer Anwendung nachhaltig verbessern.
06/2026

SQL Indizes Datenbankabfragen effizient optimieren

Blauer Pfeil nach rechts (Verlinkung)
TUnit im Praxistest: das moderne .NET-Test-Framework mit Source-Generierung, paralleler Ausführung & Native-AOT für schnellere Tests.
06/2026

NuGet Showcase: TUnit, das moderne .NET-Test-Framework

Blauer Pfeil nach rechts (Verlinkung)