TypeScript API: Language Service TypeScript API: Language Servicetypescript language-service

In this post, we'll walk through a demo of a TypeScript language service. This demo highlights how to perform real-time code diagnostics. We're using TypeScript API version 5.5.4.

Demo

Type your TypeScript code in the Source area. As you type, the Diagnostics area will display any errors or suggestions that the language service finds. You can also see the behind-the-scenes API calls in the Trace area.

Integrating TypeScript Language Service

We'll create a languageHost object with just the essential methods the language service needs to function. For now, we've kept things simple with a single file. We'll explore more advanced scenarios and expand the capabilities of our language service in future posts.

The LanguageServiceHost acts as an intermediary between the language service and the environment in which it operates (e.g., a code editor, a build tool).

This language host will be used to create a language service. This service provides core features for tasks like parsing, analyzing, and modifying your TypeScript code. When the language service needs to perform an operation that requires information about the project or its files, it queries the LanguageServiceHost for the necessary data.

import ts from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';

const file = 'index.ts';
let version = 0;

const languageHost = traceMethods({
	getCompilationSettings: () => {
		return {};
	},
	getScriptVersion: fileName => {
		return version.toString();
	},
	getScriptSnapshot: fileName => {
		return fileName === file
			? ts.ScriptSnapshot.fromString(source.value)
			: undefined;
	},
	getCurrentDirectory: () => {
		return '';
	},
	getScriptFileNames: () => {
		return [file];
	},
	getDefaultLibFileName: options => {
		return '';
	},
	readFile: (path, encoding) => {
		if (path === file) return source.value;
	},
	fileExists: path => {
		return path === 'index.ts';
	},
});

const ls = ts.createLanguageService(languageHost);

Diagnostics

The next step is to provide real-time feedback as the user types in the text area. To achieve this, we debounce the input events to avoid excessive computations and use the TypeScript language service to get the diagnostics:

source.oninput = () => {
	version++;
};

source.onchange = debounceFunction(() => {
	diagnostics.innerHTML = '';
	const hints = [];

	for (const rule of ls.getSyntacticDiagnostics(file))
		flatHints(rule, '', hints);
	for (const rule of ls.getSemanticDiagnostics(file))
		flatHints(rule, '', hints);
	for (const rule of ls.getSuggestionDiagnostics(file))
		flatHints(rule, '', hints);

	for (const hint of hints)
		diagnostics.append(JSON.stringify(hint, null, 2), '\n');
}, 100);

source.onchange();

This code ensures that any change in the source input triggers a re-analysis of the code. The diagnostics are displayed in the designated section, giving immediate feedback on syntax, semantics, and suggestions.

The TypeScript language service returns diagnostics as objects, which can include nested messages and chains of additional information. The flatHints function flattens these structures into a simple list of hints.

function getHint(rule, description) {
	const start = rule.start || 0;
	const end = start + (rule.length || 0);
	const source = rule.source ?? rule.file?.text;
	const value = source?.slice(start, end) || '';
	return {
		description,
		category: ts.DiagnosticCategory[rule.category],
		start,
		end,
		value,
	};
}

function flatHints(rule, msg, hints) {
	msg = msg || rule.messageText;
	if (typeof msg === 'string') hints.push(getHint(rule, msg));
	else {
		hints.push(getHint(rule, msg.messageText));
		if (msg.next)
			for (const chain of msg.next) flatHints(rule, chain, hints);
	}
}

Tracing Method Calls

To better understand how the language service works, we added a tracing mechanism that logs all method calls made by our custom language host. This is useful for debugging and understanding the flow of data within the service:

function traceMethods(obj) {
	for (const key in obj) {
		const fn = obj[key];
		if (typeof fn !== 'function') continue;

		obj[key] = (...args) => {
			let result;
			try {
				result = fn(...args);
				return result;
			} finally {
				log(
					`[${key}](${args.map(format).join(', ')}) => ${format(
						result,
					)}`,
				);
			}
		};
	}
	return obj;
}

This function intercepts each method call, logs the arguments and the return values, and appends this information to the trace log section.

Conclusion

This demo provides a basic example of how to use the TypeScript language service API. It's a great starting point for building more sophisticated tools and features. We'll explore more advanced use cases in upcoming posts.

Back to Main Page