The TypeScript Language Server: How IntelliSense Really Works
AI generated
<T>
type
TypeScript · tsserver · IDE Tooling · Developer Experience
The TypeScript Language Server
How IntelliSense, navigation and refactoring really work

Autocompletion, red squiggly lines and Rename Symbol feel like built in editor magic. Behind them runs a separate, long lived process: the TypeScript Language Server. Understanding how tsserver detects projects, answers requests and handles memory lets you fix performance problems deliberately instead of just restarting the editor and hoping.

14 min read tsserver · Language Service API · Protocol TypeScript 5.x

1. What the TypeScript Language Server is (and is not)

Anyone working with TypeScript uses the TypeScript Language Server practically every second at the editor, without ever consciously noticing the name. Every autocompletion, every red squiggly line under a type error, every quick info popup when hovering over a variable does not come from the tsc compiler itself, but from a separate process: tsserver. This process implements the TypeScript Language Server and runs continuously in the background, while tsc only starts on an explicit build invocation and then exits again.

Confusing tsc with the TypeScript Language Server often leads to wrong expectations. A developer running a type check on the command line with tsc --noEmit gets a complete but one time check of the whole project. The TypeScript Language Server instead works incrementally, keeps the state of the entire project in memory, and answers editor requests in milliseconds, because it does not recompile on every keystroke but only reanalyzes the affected parts of the program.

For Magento and Hyvä projects with TypeScript in the frontend build, this distinction is more than academic. Anyone who knows that the TypeScript Language Server is its own configurable process can deliberately set memory limits, enable logging and tackle performance problems in large monorepos instead of just restarting the editor and hoping for the best.

2. Architecture: tsserver, the Language Service and the project model

Internally the TypeScript Language Server consists of several layers. The bottom layer is the Language Service, a pure TypeScript module exported as ts.LanguageService, built on top of a Program, the source files and the type checker. The Language Service knows nothing about editors, sockets or a protocol. It only exposes functions such as getCompletionsAtPosition, getQuickInfoAtPosition or getDefinitionAtPosition, which take a file position and return a structured response.

Above that sits the actual tsserver process, which manages several Language Service instances, one per detected TypeScript project. A project is not necessarily defined via a tsconfig.json, there are also so called inferred projects for individual files without configuration. The TypeScript Language Server must therefore decide, whenever a file is opened, which project it belongs to before it can answer any request at all. This project detection is one of the most common sources of trouble in monorepos with multiple tsconfig.json files.

The third layer is the session object, which receives incoming requests from the editor, forwards them to the correct project, and serializes the response back. This separation into Language Service, project management and session is the reason the TypeScript Language Server can also be used programmatically outside an editor, more on that in section 8.

3. Editor communication: the tsserver protocol

Editors such as VS Code do not talk to tsc directly via the generic Language Server Protocol, but through an older, JSON based protocol of its own, native to the TypeScript Language Server and historically predating LSP. Every request is a JSON object with a seq number, a command name and an arguments object, sent over stdin to the tsserver process. The response comes back as a JSON line over stdout, correlated by the same seq number.

Other editors such as Neovim or Sublime Text instead usually access the same underlying TypeScript Language Server through an LSP bridge such as the community maintained package typescript-language-server. This bridge translates LSP requests into the native tsserver protocol and back, so editors with generic LSP client support get the same feature set as VS Code with its native 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 in detail: completions, quick info and signature help

The term IntelliSense bundles several independent functions of the TypeScript Language Server. Completions return, when a dot is typed after a variable, the list of possible properties and methods, derived from the inferred or declared type at exactly that code position. Quick info returns, when hovering over a symbol, its complete type signature including JSDoc comments, without the developer having to manually look up the declaration. Signature help shows, while typing a function call parenthesis, the expected parameters with their types and highlights which parameter is currently being entered.

Technically all three functions rely on the same mechanism. The TypeScript Language Server first resolves the type at the requested position through the type checker, then searches its symbol table for matching members and filters the result by visibility, that is whether a property is private, protected or public. For generic types the language server additionally resolves all type parameters in the current context before returning the completion list, which explains why completions for complex generic signatures can noticeably take longer than for simple object types.


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 calls getDefinitionAtPosition and returns the exact file and line position where a symbol was declared, even if that declaration lives in a library's .d.ts file. The TypeScript Language Server distinguishes between the definition, that is where something was declared, and the implementation, that is where a concrete class actually contains code. This becomes relevant for interfaces with multiple implementations: Go to Implementation then lists every class that actually implements an interface.

Find All References does not search the source text for the name as a string, but uses the full type checker to find every actual usage of the same symbol, even across re exports, destructuring and different import alias names. This semantic precision is exactly what sets the TypeScript Language Server apart from a plain text search. A name property on a User object is never confused with a same named property on a Product object, because both symbols carry distinct identities resolved by the type checker.

Rename Symbol uses the exact same reference search, but instead of merely displaying it, replaces every location found in the source, including import statements and destructuring. A rename across dozens of files that completes correctly within seconds would hardly be safe without the semantic analysis of the TypeScript Language Server, because a plain search and replace operation would inevitably also hit coincidentally identical but unrelated identifiers.

6. Multi project handling: tsconfig.json and project references

In a monorepo with multiple packages the TypeScript Language Server has to decide which tsconfig.json is responsible for an opened file. The search walks upward through the folder hierarchy from the file's directory until a tsconfig.json is found whose include or files entries actually cover the file. If no matching configuration is found, the TypeScript Language Server creates an inferred project that only uses default options and typically offers noticeably weaker type checking than an explicitly configured project.

references in tsconfig.json enable project references. A main project points to several sub projects, each of which can be compiled independently with composite: true. The TypeScript Language Server uses these references so that navigating across package boundaries, for example from an app into a shared library, jumps directly to the source file instead of the generated .d.ts file, as long as disableSourceOfProjectReferenceRedirect is not enabled.


// 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 on large codebases: partial semantic mode and memory limits

On projects with tens of thousands of files, the TypeScript Language Server itself becomes the bottleneck. Every request for completions or diagnostics requires the type checker to resolve the relevant part of the dependency graph, and with deeply nested import chains that can noticeably cost time before a response reaches the editor. VS Code therefore automatically enables partial semantic mode on very large projects, where only the currently open file is analyzed semantically, while the rest of the project is initially checked only syntactically.

A second, often overlooked bottleneck is the Node heap size of the TypeScript Language Server process itself. By default Node.js limits the heap to a few hundred megabytes, which for large type declarations, for example generated GraphQL types or extensive Magento GraphQL schemas, can cause noticeable delays or even crashes. The typescript.tsserver.maxTsServerMemory setting in VS Code raises this limit specifically, without changing the system's global Node configuration.


# 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. Using the Language Service programmatically

Because the TypeScript Language Server builds on the publicly exported ts.LanguageService, the same feature set, meaning IntelliSense, diagnostics and navigation, can also be used outside an editor, for example for custom lint rules, documentation generators or CI checks that need to know more than plain syntax. This requires your own implementation of ts.LanguageServiceHost, an interface that tells the Language Service which files exist, what their current content is, and which compiler options apply.

The crucial difference from using tsserver as a child process: a programmatic Language Service instance runs in the same Node process as the calling tool, with no protocol overhead over stdin and stdout at all. This is suited for batch analyses, for example checking every file in a project for a particular API usage, while the interactive TypeScript Language Server stays optimized for editor integrations with persistent state and incremental updates.


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 and the Compiler API compared

Three ways of accessing the same underlying TypeScript logic are available, each with different strengths depending on the task. The following overview places tsc, the interactive TypeScript Language Server and direct use of the Language Service API according to typical use case.

Tool Typical use case State between calls Typical usage
tsc --noEmit CI build, one time full check No persistent state Terminal, CI pipeline
TypeScript Language Server Editor IntelliSense, live diagnostics Persistent process per project VS Code, editor plugins
ts.LanguageService Custom tools, batch analyses Inside calling process, freely managed Custom scripts, linters
ts.createProgram Transformers, code generation Fresh per invocation Build tools, codegen scripts
LSP bridge (typescript-language-server) Editors without native tsserver integration Translates LSP to tsserver protocol Neovim, Sublime Text

For day to day editor usage, the interactive TypeScript Language Server is irreplaceable, because only it keeps incremental state across keystrokes. For CI gates, tsc --noEmit remains the right choice, because a fresh, deterministic full check without state from previous runs is desired. Custom tools that need specialized analyses are best served by accessing the Language Service API directly, instead of remote controlling tsserver as a child process through its protocol.

Mironsoft

TypeScript tooling, IDE performance and Magento/Hyvä integrations

Slow IntelliSense in your TypeScript project?

We analyze project structure, tsconfig settings and tsserver logs, fix performance bottlenecks in the TypeScript Language Server and, if needed, build custom tools on the Language Service API for Magento and Hyvä projects.

tsserver diagnostics

Log analysis, memory limits and project structure checked for performance problems

Monorepo setup

Project references and tsconfig structure for fast, correct navigation

Custom tooling

Custom analysis scripts built on the Language Service API

10. Summary

The TypeScript Language Server is not a compiler feature, but a standalone process called tsserver, built on the publicly exported Language Service API, answering editor requests through its own JSON protocol. IntelliSense, Go to Definition, Find All References and Rename Symbol all rely on the same type checker, which resolves symbols semantically rather than textually, making these features considerably more reliable than a plain text search.

In monorepos, the project detection of the TypeScript Language Server determines speed and accuracy, while project references enable clean navigation across package boundaries. For performance problems, a raised memory limit via maxTsServerMemory and analysis of the tsserver log both help. Anyone needing custom analysis tools can access ts.LanguageService directly, without talking to tsserver as a child process at all, and gets the same accuracy as the editor, only usable programmatically.

TypeScript Language Server - The Essentials at a Glance

tsc vs. tsserver

tsc checks once and exits. The TypeScript Language Server runs continuously, keeps project state in memory and answers incrementally.

Semantic navigation

Go to Definition, Find All References and Rename use the type checker, not text search, so they stay reliable across re exports.

Performance tuning

Raise maxTsServerMemory, enable TSS_LOG for diagnostics, use project references for clean monorepo boundaries.

Programmatic access

ts.createLanguageService with a custom LanguageServiceHost enables custom tools without protocol overhead.

11. FAQ: TypeScript Language Server

1tsc vs. TypeScript Language Server?
tsc checks once and exits. tsserver runs continuously, keeps state and answers editor requests incrementally.
2Restart Language Server in VS Code?
Command Palette: TypeScript: Restart TS Server. Ends and restarts tsserver, helps immediately with stuck IntelliSense.
3Why is IntelliSense so slow?
Large includes, deep import chains or too low a memory limit. Check TSS_LOG and raise maxTsServerMemory.
4What is an inferred project?
Without a matching tsconfig.json, the language server creates a project with default options, usually weaker type checking.
5Enable the tsserver log?
Set the TSS_LOG environment variable or use the VS Code setting typescript.tsserver.log. Shows every request and its duration.
6Use the Language Server without an editor?
Use ts.LanguageService programmatically with a custom LanguageServiceHost, for example for batch analyses or custom linters.
7Go to Definition vs. Go to Implementation?
Definition jumps to the declaration. Implementation lists concrete classes that actually implement an interface.
8Project references and navigation?
Correctly configured references jump directly to another package's source file instead of the generated .d.ts file.
9Raise the memory limit?
Set the VS Code typescript.tsserver.maxTsServerMemory setting in megabytes, raises the tsserver heap specifically.
10Do other editors use the same server?
Yes, via an LSP bridge such as typescript-language-server, which translates LSP into the native tsserver protocol.