25.4 Extracting Type Information from the Type Checker
Alright, let’s get our hands dirty. You’ve got a ts.Program and its trusty sidekick, the Type Checker (ts.createTypeChecker()). This isn’t some glorified linter; this is the engine room of the entire language service. It’s the thing that actually knows what string is, why your generic is failing, and that the property you’re trying to access on that object definitely, probably, doesn’t exist. Its job is to take all those abstract syntax trees and turn them into a coherent web of types.
The key thing to remember—the ‘aha’ moment—is that the AST from the parser is mostly just syntax. It knows you have a VariableDeclaration named myVar, but it doesn’t know that myVar is of type string. That semantic meaning, the actual type, is a layer of information the Type Checker slaps on top. Your goal is to tap into that layer.
Getting the Type of a Specific Node
This is your bread and butter. You find a node in the AST, and you ask the type checker, “What is this?” The answer is a ts.Type object, a glorious (and sometimes terrifying) data structure containing everything you could possibly want to know.
import * as ts from "typescript";
// You've already got this from your program
const program = ts.createProgram(["src/my-file.ts"], {});
const checker = program.getTypeChecker();
// Let's say we found this node in the AST
const node: ts.Node = ...; // e.g., an Identifier for a variable
const type = checker.getTypeAtLocation(node);
// Now what? It's just an object. To get a human-readable name:
const typeName = checker.typeToString(type);
console.log(`The type is: ${typeName}`); // e.g., "string" or "MyComplexInterface"
Why getTypeAtLocation and not just getType? Because types are contextual. The type of an Identifier node called length could be a string, a number, or something else entirely, depending on where it’s located in the code. The type checker needs the node’s position to resolve that context correctly.
Deconstructing a Type: Beyond the Name
The typeToString is a fantastic lie. It gives you a friendly, idiomatic name, but it often hides the truth. Your variable isn’t just a string; it’s a string that’s the result of a function call, or a literal value "hello". The ts.Type object holds these gory details.
// Is this type a literal value?
if (type.isStringLiteral()) {
console.log(`It's the literal string: "${type.value}"`);
} else if (type.isNumberLiteral()) {
console.log(`It's the literal number: ${type.value}`);
}
// Let's get real fancy. Is it a union?
if (type.isUnion()) {
console.log("It's a union of:");
// types property is an array of the constituent types
type.types.forEach(unionMember => {
console.log(` - ${checker.typeToString(unionMember)}`);
});
}
This is where you move from simple introspection to deep analysis. You can check flags to see if a type is any, unknown, or never. You can get the return type of a function signature, or the properties of an interface.
The symbol: The Type’s Best Friend
Almost every meaningful ts.Type has a symbol. This is a critical concept. If a Type is the shape (it has these properties, it’s callable), the Symbol is the declaration (where it was defined, its name, its members). You often need to dance between them.
const nodeSymbol = checker.getSymbolAtLocation(node);
if (nodeSymbol) {
// Get the type of the symbol
const symbolType = checker.getTypeOfSymbol(nodeSymbol);
// They might be the same, or they might not be. It's complicated.
// Let's say our type is an interface. We can get its properties.
const properties = type.getProperties();
console.log("Properties of the interface:");
properties.forEach(prop => {
// For each property symbol, get its type
const propType = checker.getTypeOfSymbol(prop);
console.log(` ${prop.name}: ${checker.typeToString(propType)}`);
});
}
This symbol-to-type tango is how you power real-world tools like automatic documentation generators or refactoring scripts.
Common Pitfalls and How to Avoid Them
The
undefinedType: The most common “gotcha”. You callgetTypeAtLocationon a node that, for whatever reason, doesn’t have a computable type. Maybe it’s a syntax error, maybe it’s a part of a type annotation itself. Always check if the type exists before trying to dissect it. A type with thets.TypeFlags.Anyflag is often what you get for a node with no real type.The Aliasing Problem:
typeToStringis designed for error messages, not for canonical IDs. The same type might be printed asArray<string>,string[], or a type aliasMyStringArray. If you need a stable identifier for comparison, you might need to use the type’s intrinsicflagsor a more complex hashing strategy.Performance, Performance, Performance: Traversing the type graph is expensive. The TypeScript team has optimized the heck out of it for the IDE scenario (fast, incremental checks), but your brute-force traversal of every node’s type will be slow. Cache results where possible. Don’t call
getTypeAtLocationon the same node in a loop.Questionable Choice Corner: Why is the API so complex? Because the type system is complex. The API reflects the internal compiler architecture, which was never designed for public consumption. It’s a leaky abstraction, and you’re seeing the leaks. Embrace it. The complexity is the price of admission for the immense power it gives you. You’re not just calling a function; you’re plugging directly into the compiler’s brain.