
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, wird Folgendes benötigt:
TypeScript funktioniert nicht ohne Weiteres mit Blazor oder JS Interop. Microsoft bietet hierfür das NuGet-Paket Microsoft.TypeScript.MSBuild, das den TypeScript-Compiler ausführt 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 besonders in Unternehmensprojekten sinnvoll.
Im Stammverzeichnis der Lösung wurde 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}
Jedes Projekt, das TypeScript enthält, benötigt eine tsconfig.json-Datei. Mit einer Basis-Konfiguration lassen sich wiederkehrende Standardeinstellungen zentral definieren. Das NuGet-Paket kann zwar auch über Attribute in der .csproj-Datei konfiguriert werden, aber dieser Ansatz ist, wie später gezeigt wird, flexibler.
Im Verzeichnis des TypeScript-Projekts wird anschließend eine tsconfig.json-Datei erstellt:
{
"extends": "../../tsconfig.base.json",
}
Hinweis: Der Pfad kann je nach Ordnerstruktur variieren.
Auch wenn es zunächst kontraintuitiv erscheinen mag, ermöglicht dieser Ansatz langfristig den Import etablierter Module und Deklarationsdateien sowie optional das Bundling von Paketen. Zusätzlich kann npm mehrere Workspaces erstellen, wodurch eine modulbasierte Struktur entsteht – vergleichbar mit der tsconfig.json.
Die package.json kann entweder über npm init -y oder manuell erstellt 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 ein zentrales Paketmanagement. In diesem Fall werden esbuild sowie die Deklarationsdatei für die JS-Interop eingebunden.
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 nicht geplant ist, 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 der strikte Modus verwendet wird, ist dies besonders 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.
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 Manger Libman.
Da TypeScript-Dateien kompiliert werden, ist zu beachten, 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 der Praxis als zuverlässig erwiesen hat, insbesondere wenn Skalierbarkeit für ein Projekt relevant wird und potenzielle technische Schulden vermieden werden sollen.
Mit einer vorbereiteten Lösung, die kompilierfähig ist, muss lediglich eine TypeScript-Datei erstellt und der relevante Code hinzugefügt werden.

Die Organisation von TypeScript-Dateien folgt denselben Prinzipien wie die JavaScript-Interoperabilität in Blazor. Theoretisch wird eine Zwischeninstanz eingeführt, die Typregeln explizit durchsetzt (tsconfig.json und TypeScript selbst) und den Code in JS-Dateien kompiliert. Obwohl technisch weiterhin JavaScript verwendet wird (da TypeScript eine Obermenge davon ist), muss nicht direkt damit gearbeitet werden, während gleichzeitig Typsicherheit und Entwicklerkomfort gewährleistet bleiben.
Zur Veranschaulichung wird eine isolierte Datei (Component1.razor.ts) erstellt, 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 wird. Diese ist anschließend für alle Funktionen und Ausdrücke innerhalb der Klasse zugänglich.
Mithilfe der Modulisolation wird die Klasse mit der neuen Methode aus der Interop-Schnittstelle initialisiert:
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 die Objektreferenz dieser Komponente übergeben wird, können .NET-Methoden aufgerufen werden, die mit dem JsInvokableAttribute gekennzeichnet sind – 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 unnötig komplexe Logik, um isoliertes Verhalten aus einer JS/TS-Datei zu erhalten, und begrenzt den Nutzungsumfang auf das tatsächlich Benötigte – ohne die Nuancen einer dynamisch typisierten Sprache.
Da eine JS-Objektinstanz erstellt wird, muss zudem sichergestellt werden, dass das IAsyncDisposable-Muster implementiert wird und die Klasseninstanz freigegeben wird, sobald die Komponente nicht mehr verwendet wird, um Speicherlecks oder potenzielle Überläufe zu vermeiden:
private IJSObjectReference? _classInstance;
private DotNetObjectReference<Component1>? _objRef;
public async ValueTask DisposeAsync()
{
if (_classInstance is not null)
await _classInstance.DisposeAsync();
_objRef?.Dispose();
}Auch die Entsorgung der .NET-Objektreferenz 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 zusätzliche Gestaltungsmöglichkeiten.
Allerdings gilt für serverseitige Anwendungen dasselbe Prinzip: Services, Handler etc., die IJSInterop verwenden, dürfen erst aufgerufen werden, nachdem die Komponente gerendert wurde.
Der Compiler generiert nun .map-Dateien und .js-Artefaktdateien für die Verwendung in der App. Diese sollten nicht in die Versionskontrolle (z. B. Git) aufgenommen werden, da sie die Versionierung und Codebasis 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 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 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.jsJe nach Lösung kann dies individuell 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.
Durch das NPM-Projektsetup ist nun die Syntaxhervorhebung in der IDE verfügbar.


In diesem Artikel wurde eine individuelle Einrichtung demonstriert, um TypeScript in einem Blazor-Projekt zu kompilieren – inklusive zusätzlicher Optionen für Paketverwaltung und Bibliotheksmanagement sowie der Einrichtung einer Komponente zur Nutzung von Klassenkonstruktoren.
Die zentrale Erkenntnis ist, dass das ASP.NET Core-Team hinter Blazor das bestehende Ökosystem kontinuierlich erweitert, um die Entwicklungserfahrung zu verbessern und zusätzliche Funktionalität bereitzustellen – bei gleichzeitiger Wahrung der Performance und einer symbiotischen Beziehung zwischen DOM und Laufzeitumgebung.
Dieser Ansatz reduziert den Aufwand, der durch Code ohne statische Typisierung entsteht. Gleichzeitig sollte berücksichtigt werden, dass jedes Werkzeug eigene Herausforderungen mit sich bringt, die bei unsachgemäßer Handhabung Probleme verursachen können.

Unser Geschäftsführer Tibor Csizmadia und unser Kundenbetreuer Jens Walter stehen Ihnen persönlich zur Verfügung. Profitieren Sie von unserer langjährigen Erfahrung und erhalten Sie eine kompetente Erstberatung in einem unverbindlichen Austausch.