Native Unterstützung ohne Build-Konfiguration
Deno führt .ts-Dateien direkt aus, ohne tsc, ohne ts-node und ohne node_modules. Wer aus dem Node.js-Ökosystem kommt, muss dafür ein paar Annahmen über Type-Checking, Module und Sicherheit neu justieren.
Inhaltsverzeichnis
- 1. Warum Deno TypeScript ohne Umwege ausführt
- 2. Erste Schritte: deno run ohne Vorkonfiguration
- 3. Der Compiler unter der Haube: Type-Stripping statt Type-Checking
- 4. Import Maps und URL-Imports statt node_modules
- 5. deno.json und deno.jsonc: zentrale Konfigurationsdatei
- 6. Das Permissions-Modell und was es für TypeScript-Code bedeutet
- 7. Testing mit Deno.test und TypeScript
- 8. Von deno compile bis Deno Deploy: TypeScript ausliefern
- 9. Migration von Node.js: npm-Specifier und Interop
- 10. Zusammenfassung
- 11. FAQ
1. Warum Deno TypeScript ohne Umwege ausführt
Deno wurde von Ryan Dahl, dem ursprünglichen Node.js-Schöpfer, mit der Erkenntnis gebaut, dass ein Großteil der serverseitigen JavaScript-Welt inzwischen in TypeScript schreibt. Statt TypeScript als nachträgliches Zusatzpaket zu behandeln, ist der Transpiler fester Bestandteil der Runtime selbst.
Jede Datei mit der Endung .ts, .tsx oder .mts wird beim Ausführen automatisch erkannt und ohne separaten Kompilierschritt gestartet. Es gibt keine tsconfig.json-Pflicht, keinen ts-node-Loader und keine Registrierung eines Transpilers auf der Kommandozeile.
Das senkt die Einstiegshürde für neue Projekte spürbar, verschiebt aber auch Verantwortung: Wer echtes Type-Checking vor der Auslieferung will, muss das bewusst als eigenen Schritt einplanen, weil deno run es standardmäßig nicht tut.
Für Teams, die aus einem Node.js-Umfeld mit ts-node oder tsx kommen, entfällt dadurch eine ganze Kategorie von Konfigurationsproblemen: Loader-Registrierung, Pfadauflösung und Interop zwischen ESM und CommonJS sind in Deno von Anfang an einheitlich gelöst statt projektspezifisch zusammengestückelt.
2. Erste Schritte: deno run ohne Vorkonfiguration
Ein neues Skript beginnt ohne npm init, ohne package.json und ohne Installationsschritt. Eine einzelne Datei mit typisierten Funktionen lässt sich direkt über deno run starten, inklusive Autovervollständigung in der IDE, sobald der Deno-Language-Server aktiv ist.
Berechtigungen werden explizit über Flags erteilt, etwa --allow-net für Netzwerkzugriff oder --allow-read für Dateisystemzugriff. Ohne diese Flags schlägt jeder entsprechende Aufruf zur Laufzeit mit einer klaren Fehlermeldung fehl, nicht erst still beim Deployment.
Für den täglichen Gebrauch reicht deno run --allow-net=api.example.com script.ts völlig aus, um ein typisiertes Skript mit eingeschränktem Netzwerkzugriff zu starten, ganz ohne vorherige Projektstruktur.
// greet.ts
interface Greeting {
name: string;
loud?: boolean;
}
function greet({ name, loud = false }: Greeting): string {
const message = `Hallo, ${name}!`;
return loud ? message.toUpperCase() : message;
}
console.log(greet({ name: "Team", loud: true }));
// Ausführen: deno run greet.ts
3. Der Compiler unter der Haube: Type-Stripping statt Type-Checking
Beim einfachen deno run entfernt Deno lediglich die Typannotationen aus dem Quellcode, ähnlich wie es Node.js seit Version 22 mit dem experimentellen Strip-Modus tut. Es findet dabei keine semantische Prüfung statt: fehlerhafte Typen werden schlicht ignoriert.
Für vollwertiges Type-Checking existiert der separate Befehl deno check, der den echten TypeScript-Compiler-Kern lädt und alle Fehler meldet, die auch tsc --noEmit finden würde. Dieser Schritt gehört in jede CI-Pipeline, wird aber lokal bewusst übersprungen, um die Startzeit niedrig zu halten.
Diese Trennung ist der wichtigste Unterschied zu klassischen ts-node-Setups: Geschwindigkeit beim Entwickeln, aber ein expliziter, separater Prüfschritt vor dem Merge oder Release.
In der Praxis bewährt sich ein deno check-Aufruf als Pre-Commit-Hook oder als eigener CI-Job vor dem Testlauf, damit Typfehler nicht erst in der Produktion auffallen, wenn eine falsch typisierte Funktion mit unerwarteten Werten aufgerufen wird.
4. Import Maps und URL-Imports statt node_modules
Deno importiert Module standardmäßig über vollständige URLs oder über npm:- und jsr:-Specifier, statt sie in einem lokalen node_modules-Ordner zu erwarten. Heruntergeladene Module landen in einem globalen, inhaltsadressierten Cache und werden projektübergreifend geteilt.
Import Maps in deno.json übersetzen kurze Bezeichner in vollständige Spezifizierer, sodass Importe im Code trotzdem lesbar bleiben, statt lange URLs in jeder Datei zu wiederholen.
Für bestehende npm-Pakete, die nicht ESM-nativ sind, funktioniert der npm:-Specifier meist transparent, inklusive Auflösung von CommonJS-Interop im Hintergrund.
Versionsangaben direkt im Specifier, etwa npm:zod@^3.23.0, ersetzen die Semver-Range aus package.json und machen jede Abhängigkeit an der Importstelle sofort sichtbar, statt sie in einer separaten Datei nachschlagen zu müssen.
// deno.json (Ausschnitt)
{
"imports": {
"std/": "https://deno.land/std@0.224.0/",
"zod": "npm:zod@^3.23.0"
}
}
// main.ts
import { z } from "zod";
import { parse } from "std/csv/mod.ts";
const Schema = z.object({ id: z.number(), name: z.string() });
5. deno.json und deno.jsonc: zentrale Konfigurationsdatei
Statt package.json, tsconfig.json und .eslintrc getrennt zu pflegen, bündelt deno.json Compiler-Optionen, Import Maps, Lint-Regeln, Formatierungseinstellungen und benannte Tasks in einer Datei.
Der Abschnitt compilerOptions übernimmt einen Großteil der aus tsconfig.json bekannten Felder wie strict oder lib, wirkt aber nur auf deno check, nicht auf die Ausführung selbst. Nicht unterstützte Felder wie module oder outDir werden schlicht ignoriert, da Deno weder ein eigenes Modulsystem transpiliert noch klassische Ausgabeverzeichnisse erzeugt.
Tasks unter tasks ersetzen npm-Scripts: deno task dev startet definierte Befehle genauso wie npm run dev, ohne dass dafür ein zusätzliches Tool nötig wäre.
// deno.json
{
"compilerOptions": {
"strict": true,
"lib": ["deno.window"]
},
"tasks": {
"dev": "deno run --watch --allow-net main.ts",
"test": "deno test --allow-read"
}
}
6. Das Permissions-Modell und was es für TypeScript-Code bedeutet
Deno startet jeden Prozess standardmäßig ohne Zugriff auf Dateisystem, Netzwerk, Umgebungsvariablen oder Subprozesse. Jede Erweiterung muss explizit als Flag angegeben werden, was das Prinzip geringster Rechte direkt auf der Ebene der Runtime durchsetzt.
Für TypeScript-Bibliotheken bedeutet das, dass Typdefinitionen allein keine Aussage über tatsächliche Seiteneffekte machen: Eine Funktion kann typkorrekt sein und trotzdem beim Aufruf an fehlenden Berechtigungen scheitern, wenn die Flags nicht gesetzt sind.
In der Praxis lohnt es sich, Berechtigungen so eng wie möglich zu granulieren, etwa --allow-read=./config statt eines pauschalen --allow-read, um versehentlichen Zugriff auf sensible Pfade auszuschließen.
Für Bibliotheken, die von Dritten eingebunden werden, empfiehlt sich zusätzlich eine klare Dokumentation der benötigten Flags, damit Nutzer nicht durch Ausprobieren herausfinden müssen, welche Berechtigungen ein Paket tatsächlich benötigt.
7. Testing mit Deno.test und TypeScript
Der eingebaute Testrunner deno test benötigt keine zusätzliche Bibliothek wie Jest oder Vitest. Assertions kommen aus der Standardbibliothek unter std/assert und sind bereits vollständig typisiert.
Testdateien werden typischerweise als *_test.ts oder unter tests/ abgelegt und automatisch erkannt. Snapshot-Tests, Coverage-Reports über deno test --coverage und paralleles Ausführen sind ohne weitere Konfiguration verfügbar.
Weil Testdateien ebenfalls dem Permissions-Modell unterliegen, lassen sich Tests, die Netzwerk- oder Dateisystemzugriff simulieren, gezielt mit den passenden Flags isolieren.
// greet_test.ts
import { assertEquals } from "std/assert/mod.ts";
import { greet } from "./greet.ts";
Deno.test("greet formats name correctly", () => {
assertEquals(greet({ name: "Team" }), "Hallo, Team!");
});
Deno.test("greet respects loud flag", () => {
assertEquals(greet({ name: "Team", loud: true }), "HALLO, TEAM!");
});
8. Von deno compile bis Deno Deploy: TypeScript ausliefern
deno compile bündelt ein TypeScript-Programm samt Runtime in eine einzige ausführbare Datei für die Zielplattform, ganz ohne separat installierte Deno-Installation auf dem Zielsystem. Das eignet sich besonders für CLI-Tools, die als Binary verteilt werden sollen.
Für Webdienste bietet Deno Deploy eine Edge-Runtime, die TypeScript-Handler direkt aus dem Repository ausführt, inklusive automatischer Skalierung über global verteilte Regionen hinweg.
Beide Wege teilen sich denselben Type-Stripping-Mechanismus wie deno run, weshalb ein vorheriger deno check-Lauf in der CI-Pipeline vor dem Deployment dringend empfehlenswert bleibt.
# Kompiliert main.ts zu einer eigenständigen Executable
deno compile --allow-net --output mein-tool main.ts
./mein-tool
9. Migration von Node.js: npm-Specifier und Interop
Eine vollständige Neuschreibung ist für die Migration selten nötig. Der npm:-Specifier erlaubt es, bestehende npm-Abhängigkeiten direkt zu importieren, während projektinterner Code schrittweise auf native Deno-APIs umgestellt wird.
Node-spezifische APIs wie fs, path oder process stehen über das node:-Präfix zur Verfügung, was Portierungen von Node.js-Skripten erheblich vereinfacht, ohne jede Zeile neu schreiben zu müssen.
Für den umgekehrten Weg unterstützt Node.js seit Version 22 experimentell das direkte Ausführen von TypeScript-Dateien über Type-Stripping, was die Kluft zwischen beiden Runtimes bei der reinen TS-Ausführung zunehmend verkleinert.
Ein realistischer Migrationspfad beginnt selten mit dem gesamten Bestand: Neue CLI-Tools, Edge-Funktionen oder isolierte Skripte eignen sich als erster Schritt, während der bestehende Node.js-Kern zunächst unverändert weiterläuft.
// Node-APIs und npm-Pakete gemeinsam nutzen
import { readFileSync } from "node:fs";
import { z } from "npm:zod@^3.23.0";
const raw = readFileSync("./config.json", "utf-8");
const Config = z.object({ port: z.number() });
const config = Config.parse(JSON.parse(raw));
| Merkmal | Deno | Node.js + ts-node | Bun |
|---|---|---|---|
| TypeScript-Unterstützung | Nativ, ohne Zusatzpaket | Nur über ts-node/tsx als Zusatzpaket | Nativ, eigener Transpiler |
| Type-Checking beim Start | Deaktiviert, separat via deno check |
Abhängig von ts-node-Konfiguration | Deaktiviert, kein eingebauter Checker |
| Package-Management | URL-/npm-Imports, kein node_modules nötig | npm mit node_modules | npm-kompatibel, eigener Installer |
| Sicherheitsmodell | Explizite Permissions (--allow-read etc.) | Kein eingebautes Sandboxing | Kein eingebautes Sandboxing |
| Kompilierung zu Binary | deno compile erzeugt Single-Executable |
Nicht nativ, externe Tools nötig | bun build --compile verfügbar |
Mironsoft
TypeScript-Migration, Typsicherheit und Team-Onboarding
JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?
Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.
Migrations-Fahrplan
Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.
Strict-Mode-Einführung
tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.
Team-Onboarding
Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.
10. Zusammenfassung
TypeScript mit Deno
Kein Build-Schritt
Type-Stripping direkt zur Laufzeit
Sicherheits-Sandbox
Explizite Permissions statt Vollzugriff
npm-Kompatibilität
npm:-Specifier für bestehende Pakete
Single Binary
deno compile für Executables