Skip to Content
Course content

87: Language Service API Basics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first dive into the TypeScript Language Service API, it's easy to assume it works like the tsc compiler: you feed it a file, and it gives you errors. But the Language Service is designed for IDEs. It's a "pull" system, not a "push" system. If you don't set up the communication channel between the service and your files correctly, the API will just return empty arrays and leave you wondering why your code isn't being analyzed.

import ts from 'typescript';

const code = 'const x: number = "I am a string";';
const fileName = 'example.ts';

// A naive attempt at a Language Service Host
const host: ts.LanguageServiceHost = {
    getScriptFileNames: () => [fileName],
    getScriptVersion: () => '1',
    getScriptSnapshot: (fileName) => {
        return ts.createTextSnapshot(code);
    },
    getTypeScriptVersion: () => ts.version,
    // We forgot to implement getScriptFile!
};

const service = ts.createLanguageService(host);
const diagnostics = service.getSemanticDiagnostics(fileName);

console.log(diagnostics); // Result: [] (Empty array!)

The Silence of the Diagnostics

Looking at the code above, you'd expect an error because we're assigning a string to a number. But the result is an empty array. Why? Because the Language Service is lazy—in a good way. To save memory in a massive project, it doesn't load everything. When it needs to actually analyze the semantics of a file, it asks the host for the SourceFile object via the getScriptFile method.

In the broken example, I omitted getScriptFile. When the service tries to check for type errors, it asks the host, "Can you give me the parsed AST for example.ts?" The host returns undefined, and the service simply gives up on that file. It doesn't throw an error; it just assumes there's nothing to check.

Bridging the Host and the Source

To fix this, we need to implement getScriptFile. This method is where the raw text (the snapshot) gets turned into a ts.SourceFile (the AST). I usually recommend using a simple cache here, because parsing the same string into an AST over and over again will tank your performance as soon as your "mini-IDE" grows beyond one file.

import ts from 'typescript';

const code = 'const x: number = "I am a string";';
const fileName = 'example.ts';

const host: ts.LanguageServiceHost = {
    getScriptFileNames: () => [fileName],
    getScriptVersion: () => '1',
    getScriptSnapshot: (fileName) => ts.createTextSnapshot(code),
    getTypeScriptVersion: () => ts.version,
    
    // The missing link:
    getScriptFile: (fileName) => {
        // In a real app, cache this SourceFile object!
        return ts.createSourceFile(
            fileName, 
            code, 
            ts.ScriptTarget.Latest, 
            true
        );
    }
};

const service = ts.createLanguageService(host);
const diagnostics = service.getSemanticDiagnostics(fileName);

console.log(diagnostics.map(d => d.messageText)); 
// Result: ["Type 'string' is not assignable to type 'number'."]

Now the service has a path: it asks for the snapshot to see if the file has changed, and it asks for the SourceFile to actually perform the type checking. I've found that most developers struggle here because the LanguageServiceHost interface is huge and intimidating, but for basic in-memory tools, these are the only few methods that actually matter.

Semantic vs. Syntactic Diagnostics

One last thing to keep in mind: notice I used getSemanticDiagnostics. If you had a syntax error (like a missing closing brace), getSemanticDiagnostics might return nothing because the code isn't even valid enough to check types. If you're building a real-time linter, you'll want to call both getSyntacticDiagnostics (for "your code is broken") and getSemanticDiagnostics (for "your types are wrong").




📋 Practical Task

Building a "Type-Mismatch" Detector

Your task is to create a small utility script using the TypeScript Language Service API. This utility should take a string of TypeScript code and a target variable name, and determine if that specific variable is being assigned a value of the wrong type.

Requirements:

  • Implement a LanguageServiceHost that handles in-memory strings.
  • Use service.getSemanticDiagnostics(fileName) to retrieve type errors.
  • Filter the diagnostics to find any error that occurs on the line where your target variable is defined.
  • The script should output "Type Error Found" if the variable has a type mismatch, or "Type OK" otherwise.

Test Case:

const inputCode = 'let age: number = "twenty";';
const targetVar = 'age'; 
// Should output: "Type Error Found"
Rating
0 0

There are no comments for now.

to be the first to leave a comment.