To preserve narrowing for a child node, check the child itself with the matching TypeScript Compiler API predicate—such as ts.isIdentifier—and use the narrowed value in the same control-flow branch. For reusable checks, give the helper a concrete type-predicate return type and delegate its runtime test to the built-in guard when one exists.
What a reusable type guard promises
A user-defined type guard has a return type shaped like parameterName is Type. That predicate tells TypeScript that when the function returns true, the argument is of the specified type. The implementation must therefore perform a runtime check that actually establishes the promise; a type predicate is not a substitute for that check.
As an Amazon Associate I earn from qualifying purchases.
The TypeScript Handbook describes the pattern this way: “To define a user-defined type guard, we simply need to define a function whose return type is a type predicate:” TypeScript Handbook: Narrowing.
Recommended Free Tools
Prefer the Compiler API’s built-in node guards
The Compiler API provides predicates such as ts.isIdentifier and ts.isCallExpression. Use the predicate that matches the node type you need rather than rebuilding its runtime test or asserting a type from a SyntaxKind check that does not prove the full type.
#1 Best Overall
import * as ts from "typescript";
function isIdentifier(node: ts.Node): node is ts.Identifier {
return ts.isIdentifier(node);
}
This wrapper can give callers a named, reusable predicate while relying on the Compiler API for the actual test. If no built-in guard matches, make sure your custom condition truly verifies every part of the type you promise.
Check the child value you intend to use
A parent-node guard narrows the parent; it does not automatically narrow each child beyond the type declared for that child property. Once the parent is known, run the relevant guard on the child value and use that same value inside the successful branch.
Rank #2
- TypeScript implements a superset of syntax for strictly typed development, facilitating deep static analysis and enhanced development environment integration. The compiler translates source into standard script formats, ensuring parity across any runtime.
- TypeScript is ideal for front-end developers, full-stack engineers, and software architects who build large-scale web applications. It serves those looking to improve code excellence, reduce bugs through static checking, and maintain complex projects more.
- Lightweight, Classic fit, Double-needle sleeve and bottom hem
function inspect(node: ts.Node): void {
if (ts.isCallExpression(node)) {
const expression = node.expression;
if (isIdentifier(expression)) {
// expression is ts.Identifier here.
console.log(expression.text);
}
}
}
Here, the call-expression check makes node.expression available as the call’s expression. The local const gives the guard and later access a stable value for TypeScript’s control-flow analysis. The exact declared type of a child property depends on the parent node and the TypeScript declarations installed in the project.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Keep traversal and narrowing distinct
The Compiler API guide describes Node as the AST’s basic building block and demonstrates recursive traversal with ts.forEachChild. It also shows built-in guards such as ts.isClassDeclaration(node) before declaration-specific properties are accessed. Use traversal to visit nodes, then apply a node guard to the particular value whose type you need; a type checker is not required merely to walk syntax.
Use semantic checker APIs when the task requires symbol or type information, not just because the code is traversing an AST. See Microsoft’s Using the Compiler API guide.
Why TypeScript can lose a narrowing
TypeScript narrows types through control-flow analysis: it tracks checks and reachability through a branch. The fact applies to the value that was checked, and changes to that value can make the earlier fact unusable. Reassigning a variable, mutating a property, or checking one expression and later using a different one can break the connection between check and use.
TypeScript 4.4 documented that extracted conditions can preserve narrowing in stable cases, including when the guarded value is a const, a readonly property, or an unmodified parameter. That does not mean every condition extracted into a variable, closure, or mutable-property access preserves narrowing in every compiler version. Keep the check and use close, and verify behavior with the project’s installed TypeScript version. See the TypeScript 4.4 release notes.
Choose the simplest safe guard shape
| Choice | When it fits | Trade-off |
|---|---|---|
Built-in ts.isX predicate |
A matching Compiler API guard exists. | Directly communicates the node type and uses the API’s runtime test. |
| Concrete wrapper predicate | Callers benefit from a named helper such as isIdentifier. |
Its predicate annotation is straightforward; its implementation should delegate to or correctly reproduce the runtime check. |
| Direct child check | The child is used immediately in the branch. | Concise, though repeating a complex property expression can make the checked value less obvious. |
Local const plus check |
The child is used more than once or the expression is easier to track by name. | Adds a local variable but makes it clear which stable value is being narrowed. |
| Generic guard wrapper | A project has a real need for a shared abstraction and validates it against its compiler version. | Inference and predicate signatures can be compiler-version-sensitive; no single generic signature is established here as sound and stable across releases. |
Check Compiler API compatibility before copying examples
Compiler API declarations and behavior are tied to the TypeScript version installed in the project. The Microsoft wiki page for Using the Compiler API warns that its contents describe TypeScript 6.0 and earlier and that TypeScript 7.1 will have a different API. Check the version in your project before adopting examples, particularly when moving across that transition.
Best Value
TypeScript 4.9 release notes report that the TypeScript team’s refactoring of forEachChild yielded up to a 20% reduction in time spent in the binding phase and across language-service operations, and up to a 3% reduction in time generating project output. These are compiler-team results for that refactoring, not benchmarks of custom traversal code. See the TypeScript 4.9 release notes.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




