Der TypeScript Language Server: Wie IntelliSense wirklich funktioniert
AI generated
<T>
type
TypeScript · tsserver · IDE Tooling · Developer Experience
Der TypeScript Language Server
Wie IntelliSense, Navigation und Refactoring wirklich funktionieren

Autovervollständigung, rote Wellenlinien und Rename Symbol wirken im Editor wie eingebaute Magie. Tatsächlich steckt dahinter ein eigener, dauerhaft laufender Prozess: der TypeScript Language Server. Wer versteht, wie tsserver Projekte erkennt, Anfragen beantwortet und mit Speicher umgeht, kann Performance-Probleme gezielt lösen statt den Editor einfach neu zu starten.

14 Min. Lesezeit tsserver · Language Service API · Protokoll TypeScript 5.x

1. Was der TypeScript Language Server ist (und was er nicht ist)

Wer mit TypeScript arbeitet, nutzt den TypeScript Language Server in praktisch jeder Sekunde am Editor, ohne den Namen jemals bewusst wahrzunehmen. Jede Autovervollständigung, jede rote Wellenlinie unter einem Typfehler, jedes Quick-Info-Popup beim Hovern über eine Variable stammt nicht vom Compiler tsc selbst, sondern von einem separaten Prozess: tsserver. Dieser Prozess implementiert den TypeScript Language Server und läuft dauerhaft im Hintergrund, während tsc nur beim expliziten Build-Aufruf startet und danach wieder beendet wird.

Die Verwechslung von tsc und dem TypeScript Language Server führt häufig zu falschen Erwartungen. Ein Entwickler, der eine Typprüfung in der Kommandozeile mit tsc --noEmit ausführt, bekommt eine vollständige, aber einmalige Prüfung des gesamten Projekts. Der TypeScript Language Server dagegen arbeitet inkrementell, hält den Zustand des gesamten Projekts im Speicher und beantwortet Editor-Anfragen in Millisekunden, weil er nicht bei jeder Tastatureingabe neu kompiliert, sondern nur die betroffenen Teile des Programms neu analysiert.

Für Magento- und Hyvä-Projekte mit TypeScript-Anteilen im Frontend-Build ist dieses Verständnis mehr als akademisch. Wer weiss, dass der TypeScript Language Server ein eigener, konfigurierbarer Prozess ist, kann gezielt Speicherlimits setzen, Logging aktivieren und Performance-Probleme in grossen Monorepos gezielt angehen, statt pauschal den Editor neu zu starten und auf Besserung zu hoffen.

2. Architektur: tsserver, Language Service und das Projekt-Modell

Intern besteht der TypeScript Language Server aus mehreren Schichten. Die unterste Schicht ist der Language Service, ein reines TypeScript-Modul, das als ts.LanguageService exportiert wird und auf einem Program, den Sourcefiles und dem Type Checker aufbaut. Der Language Service kennt keine Editoren, keine Sockets und kein Protokoll. Er stellt lediglich Funktionen bereit wie getCompletionsAtPosition, getQuickInfoAtPosition oder getDefinitionAtPosition, die eine Datei-Position entgegennehmen und eine strukturierte Antwort liefern.

Darüber liegt der eigentliche tsserver-Prozess, der mehrere Language-Service-Instanzen verwaltet, jeweils eine pro erkanntem TypeScript-Projekt. Ein Projekt wird dabei nicht zwingend über eine tsconfig.json definiert, es gibt auch sogenannte inferred projects für einzelne Dateien ohne Konfiguration. Der TypeScript Language Server muss deshalb bei jedem Öffnen einer Datei zunächst entscheiden, zu welchem Projekt sie gehört, bevor er überhaupt eine Anfrage beantworten kann. Diese Projekterkennung ist eine der häufigsten Fehlerquellen bei Monorepos mit mehreren tsconfig.json-Dateien.

Die dritte Schicht ist das Session-Objekt, das eingehende Anfragen vom Editor entgegennimmt, an das richtige Projekt weiterleitet und die Antwort zurückserialisiert. Diese Trennung in Language Service, Projektverwaltung und Session ist der Grund, warum sich der TypeScript Language Server auch ausserhalb eines Editors programmatisch nutzen lässt, dazu mehr in Abschnitt 8.

3. Editor-Kommunikation: das tsserver-Protokoll

Editoren wie VS Code kommunizieren nicht über das generische Language Server Protocol direkt mit tsc, sondern über ein eigenes, älteres JSON-basiertes Protokoll, das dem TypeScript Language Server eigen ist und historisch vor LSP entstanden ist. Jede Anfrage ist ein JSON-Objekt mit einer seq-Nummer, einem command-Namen und einem arguments-Objekt, gesendet über stdin an den tsserver-Prozess. Die Antwort kommt als JSON-Zeile über stdout zurück, korreliert über dieselbe seq-Nummer.

Andere Editoren wie Neovim oder Sublime Text greifen stattdessen meist über eine LSP-Brücke wie das community-gepflegte Paket typescript-language-server auf denselben zugrunde liegenden TypeScript Language Server zu. Diese Brücke übersetzt LSP-Anfragen in das native tsserver-Protokoll und zurück, sodass Editoren mit generischem LSP-Client-Support denselben Funktionsumfang bekommen wie VS Code mit seiner nativen Integration.


// Request sent to tsserver via stdin (simplified, real payload has more fields)
{
  "seq": 12,
  "type": "request",
  "command": "completions",
  "arguments": {
    "file": "/project/src/checkout.ts",
    "line": 42,
    "offset": 18,
    "prefix": "getT"
  }
}

// Response read from tsserver via stdout, correlated by "request_seq"
{
  "seq": 0,
  "type": "response",
  "command": "completions",
  "request_seq": 12,
  "success": true,
  "body": [
    { "name": "getTotal", "kind": "method", "sortText": "11" },
    { "name": "getTaxRate", "kind": "method", "sortText": "11" }
  ]
}

4. IntelliSense im Detail: Completions, Quick Info und Signature Help

Der Begriff IntelliSense fasst mehrere unabhängige Funktionen des TypeScript Language Server zusammen. Completions liefern beim Tippen eines Punkts nach einer Variable die Liste möglicher Properties und Methoden, abgeleitet aus dem inferierten oder deklarierten Typ an genau dieser Codeposition. Quick Info liefert beim Hovern über ein Symbol dessen vollständige Typsignatur inklusive JSDoc-Kommentar, ohne dass der Entwickler die Deklaration manuell aufsuchen muss. Signature Help zeigt während des Tippens einer Funktionsaufrufklammer die erwarteten Parameter samt Typen und markiert, welcher Parameter aktuell eingegeben wird.

Technisch beruhen alle drei Funktionen auf demselben Mechanismus. Der TypeScript Language Server löst zunächst den Typ an der angefragten Position über den Type Checker auf, durchsucht dann dessen Symbol-Tabelle nach passenden Mitgliedern und filtert das Ergebnis nach Sichtbarkeit, also ob eine Property private, protected oder public ist. Bei generischen Typen löst der Language Server zusätzlich alle Typparameter im aktuellen Kontext auf, bevor die Completion-Liste zurückgegeben wird, was erklärt, warum Completions bei komplexen generischen Signaturen spürbar länger dauern können als bei einfachen Objekttypen.


import ts from "typescript";

// Minimal, self-contained demonstration of what tsserver does internally
function inspectAt(fileName: string, source: string, position: number) {
  const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
  const host: ts.LanguageServiceHost = {
    getScriptFileNames: () => [fileName],
    getScriptVersion: () => "1",
    getScriptSnapshot: (name) =>
      name === fileName ? ts.ScriptSnapshot.fromString(source) : undefined,
    getCurrentDirectory: () => process.cwd(),
    getCompilationSettings: () => ({ strict: true, target: ts.ScriptTarget.Latest }),
    getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
    fileExists: ts.sys.fileExists,
    readFile: ts.sys.readFile,
  };

  const languageService = ts.createLanguageService(host, ts.createDocumentRegistry());

  // Same calls tsserver makes internally for hover and completions
  const quickInfo = languageService.getQuickInfoAtPosition(fileName, position);
  const completions = languageService.getCompletionsAtPosition(fileName, position, {});

  console.log(quickInfo?.displayParts?.map((p) => p.text).join(""));
  console.log(completions?.entries.map((e) => e.name).slice(0, 5));
}

Go to Definition ruft getDefinitionAtPosition auf und liefert die exakte Datei- und Zeilenposition, an der ein Symbol deklariert wurde, selbst wenn diese Deklaration in einer .d.ts-Datei einer Bibliothek liegt. Der TypeScript Language Server unterscheidet dabei zwischen der Definition, also wo etwas deklariert wurde, und der Implementierung, also wo eine konkrete Klasse tatsächlich Code enthält. Das wird bei Interfaces mit mehreren Implementierungen relevant: Go to Implementation listet dann alle Klassen, die ein Interface tatsächlich umsetzen.

Find All References durchsucht nicht den Quelltext nach dem Namen als String, sondern nutzt den vollständigen Type Checker, um jede tatsächliche Nutzung desselben Symbols zu finden, auch über Re-Exports, Destrukturierung und unterschiedliche Import-Aliasnamen hinweg. Genau diese semantische Präzision unterscheidet den TypeScript Language Server von einer einfachen Textsuche. Eine Property name auf einem User-Objekt wird nicht mit einer gleichnamigen Property auf einem Product-Objekt verwechselt, weil beide Symbole unterschiedliche, vom Type Checker aufgelöste Identitäten besitzen.

Rename Symbol nutzt exakt dieselbe Referenzsuche, ersetzt aber statt einer reinen Anzeige jede gefundene Stelle im Quelltext, inklusive Import-Statements und Destrukturierungen. Ein Rename über Dutzende Dateien hinweg, der in Sekunden korrekt abgeschlossen ist, wäre ohne die semantische Analyse des TypeScript Language Server kaum sicher möglich, weil eine reine Suchen-und-Ersetzen-Operation zwangsläufig auch zufällig gleichlautende, aber inhaltlich unabhängige Bezeichner treffen würde.

6. Multi-Projekt-Handling: tsconfig.json und Project References

In einem Monorepo mit mehreren Paketen muss der TypeScript Language Server entscheiden, welche tsconfig.json für eine geöffnete Datei zuständig ist. Die Suche erfolgt vom Verzeichnis der Datei aus aufwärts durch die Ordnerhierarchie, bis eine tsconfig.json gefunden wird, deren include- oder files-Angaben die Datei tatsächlich erfassen. Fehlt eine passende Konfiguration, legt der TypeScript Language Server ein inferred project an, das lediglich Standardoptionen verwendet und typischerweise deutlich schwächere Typprüfung bietet als ein explizit konfiguriertes Projekt.

references in der tsconfig.json ermöglichen Project References. Ein Hauptprojekt verweist auf mehrere Teilprojekte, die jeweils unabhängig mit composite: true kompiliert werden können. Der TypeScript Language Server nutzt diese Referenzen, um beim Navigieren über Paketgrenzen hinweg, etwa von einer App in eine gemeinsam genutzte Library, direkt zur Quelldatei statt zur generierten .d.ts-Datei zu springen, sofern disableSourceOfProjectReferenceRedirect nicht aktiviert ist.


// Root tsconfig.json for a monorepo with two packages
{
  "files": [],
  "references": [
    { "path": "./packages/shared-types" },
    { "path": "./packages/storefront-app" }
  ]
}

// packages/shared-types/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

// packages/storefront-app/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist"
  },
  "references": [{ "path": "../shared-types" }],
  "include": ["src"]
}

7. Performance bei grossen Codebasen: Partial Semantic Mode und Speicherlimits

Bei Projekten mit mehreren Zehntausend Dateien wird der TypeScript Language Server selbst zum Flaschenhals. Jede Anfrage nach Completions oder Diagnostics erfordert, dass der Type Checker den relevanten Teil des Abhängigkeitsgraphen auflöst, und bei tief verschachtelten Importketten kann das spürbar Zeit kosten, bevor eine Antwort beim Editor ankommt. VS Code aktiviert deshalb bei sehr grossen Projekten automatisch den Partial Semantic Mode, bei dem nur die aktuell geöffnete Datei semantisch analysiert wird, während der Rest des Projekts zunächst nur syntaktisch geprüft wird.

Ein zweiter, häufig übersehener Engpass ist die Node-Heap-Grösse des TypeScript Language Server-Prozesses selbst. Standardmässig begrenzt Node.js den Heap auf einige hundert Megabyte, was bei grossen Typdeklarationen, etwa aus generierten GraphQL-Typen oder umfangreichen Magento-GraphQL-Schemas, zu spürbaren Verzögerungen oder sogar Abstürzen führen kann. Die Einstellung typescript.tsserver.maxTsServerMemory in VS Code erhöht dieses Limit gezielt, ohne die globale Node-Konfiguration des Systems zu verändern.


# Enable verbose tsserver logging for troubleshooting (writes to a temp file)
export TSS_LOG="-level verbose -file /tmp/tsserver.log"

# Tail the log while reproducing a slow completion in the editor
tail -f /tmp/tsserver.log | grep -E "completions|Req [0-9]+ took"

# Find requests that took longer than 500ms
grep -E "took [5-9][0-9]{2}ms|took [0-9]{4,}ms" /tmp/tsserver.log

# VS Code setting to raise the tsserver heap limit (settings.json excerpt)
# "typescript.tsserver.maxTsServerMemory": 4096

8. Den Language Service programmatisch nutzen

Weil der TypeScript Language Server auf dem öffentlich exportierten ts.LanguageService aufbaut, lässt sich derselbe Funktionsumfang, also IntelliSense, Diagnostics und Navigation, auch ausserhalb eines Editors nutzen, etwa für eigene Lint-Regeln, Dokumentationsgeneratoren oder CI-Checks, die mehr wissen müssen als reine Syntax. Voraussetzung ist eine eigene Implementierung von ts.LanguageServiceHost, einem Interface, das dem Language Service mitteilt, welche Dateien existieren, wie ihr Inhalt aktuell lautet und welche Compiler-Optionen gelten.

Der entscheidende Unterschied zur Nutzung von tsserver als Kindprozess: Eine programmatische Language-Service-Instanz läuft im selben Node-Prozess wie das aufrufende Tool, ganz ohne Protokoll-Overhead über stdin und stdout. Das eignet sich für Batch-Analysen, etwa das Prüfen aller Dateien eines Projekts auf eine bestimmte API-Nutzung, während der interaktive TypeScript Language Server für Editor-Integrationen mit dauerhaftem Zustand und inkrementellen Updates optimiert bleibt.


import ts from "typescript";
import fs from "node:fs";
import path from "node:path";

// Batch-check every .ts file in a directory for semantic errors,
// without spawning a tsserver process or the tsc CLI
function checkProject(rootDir: string) {
  const fileNames = fs
    .readdirSync(rootDir)
    .filter((f) => f.endsWith(".ts"))
    .map((f) => path.join(rootDir, f));

  const versions = new Map(fileNames.map((f) => [f, 1]));

  const host: ts.LanguageServiceHost = {
    getScriptFileNames: () => fileNames,
    getScriptVersion: (f) => String(versions.get(f) ?? 0),
    getScriptSnapshot: (f) =>
      fs.existsSync(f) ? ts.ScriptSnapshot.fromString(fs.readFileSync(f, "utf8")) : undefined,
    getCurrentDirectory: () => rootDir,
    getCompilationSettings: () => ({ strict: true }),
    getDefaultLibFileName: ts.getDefaultLibFilePath,
    fileExists: ts.sys.fileExists,
    readFile: ts.sys.readFile,
    readDirectory: ts.sys.readDirectory,
  };

  const service = ts.createLanguageService(host);

  for (const file of fileNames) {
    const diagnostics = service.getSemanticDiagnostics(file);
    for (const d of diagnostics) {
      console.log(`${file}: ${ts.flattenDiagnosticMessageText(d.messageText, "\n")}`);
    }
  }
}

9. tsserver, tsc und Compiler API im Vergleich

Drei Zugriffswege auf dieselbe zugrunde liegende TypeScript-Logik stehen zur Auswahl, mit unterschiedlichen Stärken je nach Aufgabe. Die folgende Übersicht ordnet tsc, den interaktiven TypeScript Language Server und die direkte Nutzung der Language Service API nach typischem Einsatzzweck ein.

Werkzeug Typischer Einsatzzweck Zustand zwischen Aufrufen Typische Nutzung
tsc --noEmit CI-Build, einmalige Vollprüfung Kein persistenter Zustand Terminal, CI-Pipeline
TypeScript Language Server Editor-IntelliSense, Live-Diagnostics Persistenter Prozess pro Projekt VS Code, Editor-Plugins
ts.LanguageService Eigene Tools, Batch-Analysen Im aufrufenden Prozess, frei verwaltet Custom Scripts, Linter
ts.createProgram Transformer, Codegenerierung Einmalig pro Aufruf Build-Tools, Codegen-Skripte
LSP-Brücke (typescript-language-server) Editor ohne native tsserver-Integration Übersetzt LSP zu tsserver-Protokoll Neovim, Sublime Text

Für den täglichen Editor-Betrieb ist der interaktive TypeScript Language Server alternativlos, weil nur er inkrementellen Zustand über Tastatureingaben hinweg hält. Für CI-Gates bleibt tsc --noEmit die richtige Wahl, weil eine frische, deterministische Vollprüfung ohne Zustand aus vorherigen Läufen gewünscht ist. Eigene Tools, die spezielle Analysen brauchen, greifen am besten direkt auf die Language Service API zu, statt tsserver als Kindprozess über sein Protokoll fernzusteuern.

Mironsoft

TypeScript-Tooling, IDE-Performance und Magento/Hyvä-Integrationen

Langsame IntelliSense im TypeScript-Projekt?

Wir analysieren Projektstruktur, tsconfig-Einstellungen und tsserver-Logs, beheben Performance-Engpässe im TypeScript Language Server und bauen bei Bedarf eigene Tools auf der Language Service API für Magento- und Hyvä-Projekte.

tsserver-Diagnose

Log-Analyse, Speicherlimits und Projektstruktur auf Performance-Probleme prüfen

Monorepo-Setup

Project References und tsconfig-Struktur für schnelle, korrekte Navigation

Custom Tooling

Eigene Analyse-Skripte auf Basis der Language Service API

10. Zusammenfassung

Der TypeScript Language Server ist kein Feature des Compilers, sondern ein eigenständiger Prozess namens tsserver, der auf der öffentlich exportierten Language Service API aufbaut und Editor-Anfragen über ein eigenes JSON-Protokoll beantwortet. IntelliSense, Go to Definition, Find All References und Rename Symbol beruhen alle auf demselben Type Checker, der Symbole semantisch statt textuell auflöst, was diese Funktionen deutlich zuverlässiger macht als eine reine Textsuche.

Bei Monorepos entscheidet die Projekterkennung des TypeScript Language Server über Geschwindigkeit und Genauigkeit, während Project References saubere Navigation über Paketgrenzen hinweg ermöglichen. Bei Performance-Problemen helfen ein erhöhtes Speicherlimit über maxTsServerMemory und die Analyse des tsserver-Logs. Wer eigene Analyse-Werkzeuge braucht, kann direkt auf ts.LanguageService zugreifen, ganz ohne tsserver als Kindprozess anzusprechen, und bekommt dieselbe Genauigkeit wie der Editor, nur programmatisch nutzbar.

TypeScript Language Server - Das Wichtigste auf einen Blick

tsc vs. tsserver

tsc prüft einmalig und beendet sich. Der TypeScript Language Server läuft dauerhaft, hält Projektzustand im Speicher und antwortet inkrementell.

Semantische Navigation

Go to Definition, Find All References und Rename nutzen den Type Checker, nicht Textsuche, deshalb sind sie über Re-Exports hinweg zuverlässig.

Performance-Tuning

maxTsServerMemory erhöhen, TSS_LOG für Diagnose aktivieren, Project References für saubere Monorepo-Grenzen nutzen.

Programmatischer Zugriff

ts.createLanguageService mit eigenem LanguageServiceHost ermöglicht Custom-Tools ohne Protokoll-Overhead.

11. FAQ: TypeScript Language Server

1tsc vs. TypeScript Language Server?
tsc prüft einmalig und beendet sich. tsserver läuft dauerhaft, hält Zustand und antwortet Editor-Anfragen inkrementell.
2Language Server in VS Code neu starten?
Command Palette: TypeScript: Restart TS Server. Beendet und startet tsserver neu, hilft bei hängender IntelliSense sofort.
3Warum ist IntelliSense so langsam?
Grosse Includes, tiefe Importketten oder zu niedriges Speicherlimit. TSS_LOG prüfen und maxTsServerMemory erhöhen.
4Was ist ein inferred project?
Ohne passende tsconfig.json legt der Language Server ein Projekt mit Standardoptionen an, meist mit schwächerer Typprüfung.
5tsserver-Log aktivieren?
Umgebungsvariable TSS_LOG setzen oder VS-Code-Einstellung typescript.tsserver.log nutzen. Zeigt jede Anfrage und ihre Dauer.
6Language Server ohne Editor nutzen?
ts.LanguageService programmatisch mit eigenem LanguageServiceHost nutzen, etwa für Batch-Analysen oder eigene Linter.
7Go to Definition vs. Go to Implementation?
Definition springt zur Deklaration. Implementation listet konkrete Klassen, die ein Interface tatsächlich umsetzen.
8Project References und Navigation?
Korrekt konfigurierte references springen direkt zur Quelldatei anderer Packages statt zur generierten .d.ts-Datei.
9Speicherlimit erhöhen?
VS-Code-Einstellung typescript.tsserver.maxTsServerMemory in Megabyte setzen, erhöht gezielt den tsserver-Heap.
10Nutzen andere Editoren denselben Server?
Ja, über eine LSP-Brücke wie typescript-language-server, die LSP in das native tsserver-Protokoll übersetzt.