Utility functions for working with TypeScript's API, providing comprehensive tools for analyzing and manipulating TypeScript AST nodes, types, and compiler APIs.
79
Usage Analysis is an advanced capability of ts-api-utils that provides comprehensive tools for analyzing variable and symbol usage patterns within TypeScript source files. This module enables sophisticated code analysis by tracking how identifiers are declared and referenced across different scopes and domains, making it essential for building linters, refactoring tools, and other advanced TypeScript tooling.
TypeScript code analysis often requires understanding not just what variables and types are declared, but how they are used throughout a codebase. The Usage Analysis module provides a systematic approach to:
This capability is particularly valuable for tools that need to understand identifier lifecycles, detect unused variables, analyze dependencies, or perform safe refactoring operations.
TypeScript operates with multiple "domains" or "spaces" where identifiers can exist:
Understanding these domains is crucial because the same identifier name can exist in different domains simultaneously without conflict.
The system tracks two primary aspects of identifier usage:
Variable usage is analyzed within the context of TypeScript's scoping rules, including:
The main entry point for variable usage analysis.
Creates a comprehensive mapping of each declared identifier to its usage information within a source file.
function collectVariableUsage(sourceFile: ts.SourceFile): Map<ts.Identifier, UsageInfo> { .api }Parameters:
sourceFile: ts.SourceFile - The TypeScript source file to analyzeReturns: A Map where keys are identifier nodes and values contain complete usage information including declarations, references, domains, and scope details.
Example:
import { collectVariableUsage } from 'ts-api-utils';
declare const sourceFile: ts.SourceFile;
const usage = collectVariableUsage(sourceFile);
for (const [identifier, information] of usage) {
console.log(`${identifier.getText()} is used ${information.uses.length} time(s).`);
console.log(` Declared in domain: ${information.domain}`);
console.log(` Exported: ${information.exported}`);
console.log(` Global scope: ${information.inGlobalScope}`);
}Data structures that represent how variables and types are used throughout code.
Contains comprehensive information about how an identifier was declared and referenced.
interface UsageInfo {
declarations: ts.Identifier[];
domain: DeclarationDomain;
exported: boolean;
inGlobalScope: boolean;
uses: Usage[];
} { .api }Properties:
declarations: ts.Identifier[] - All locations where the identifier was declareddomain: DeclarationDomain - Which domain(s) the identifier exists inexported: boolean - Whether the identifier was exported from its module/namespace scopeinGlobalScope: boolean - Whether any declaration was in the global scopeuses: Usage[] - All references to the identifier throughout the fileRepresents a single instance of an identifier being referenced.
interface Usage {
domain: UsageDomain;
location: ts.Identifier;
} { .api }Properties:
domain: UsageDomain - Which domain(s) the usage occurs inlocation: ts.Identifier - The AST node representing the usage locationEnumeration defining the different domains where identifiers can be declared.
enum DeclarationDomain {
Namespace = 1,
Type = 2,
Value = 4,
Any = 7, // Namespace | Type | Value
Import = 8
} { .api }Values:
Namespace - Declared in namespace space (modules, namespace declarations)Type - Declared in type space (interfaces, type aliases, type parameters)Value - Declared in value space (variables, functions, classes)Any - Exists in all three primary domains (enums, classes)Import - Special flag for imported identifiersEnumeration defining the different domains where identifier usage can occur.
enum UsageDomain {
Namespace = 1,
Type = 2,
Value = 4,
Any = 7, // Namespace | Type | Value
TypeQuery = 8, // typeof usage
ValueOrNamespace = 5 // Value | Namespace
} { .api }Values:
Namespace - Used as a namespace referenceType - Used in a type positionValue - Used as a runtime valueAny - Could be used in any domainTypeQuery - Used in typeof expressionsValueOrNamespace - Ambiguous value or namespace usageExample:
import { collectVariableUsage, DeclarationDomain, UsageDomain } from 'ts-api-utils';
function analyzeIdentifierUsage(sourceFile: ts.SourceFile) {
const usage = collectVariableUsage(sourceFile);
for (const [identifier, info] of usage) {
const name = identifier.getText();
// Check what domains the identifier is declared in
if (info.domain & DeclarationDomain.Type) {
console.log(`${name} is declared as a type`);
}
if (info.domain & DeclarationDomain.Value) {
console.log(`${name} is declared as a value`);
}
// Analyze usage patterns
const typeUsages = info.uses.filter(use => use.domain & UsageDomain.Type);
const valueUsages = info.uses.filter(use => use.domain & UsageDomain.Value);
console.log(`${name} used ${typeUsages.length} time(s) in type positions`);
console.log(`${name} used ${valueUsages.length} time(s) in value positions`);
}
}Functions for understanding the context and domain of identifier usage.
Determines which domain(s) an identifier usage occurs within based on its syntactic context.
function getUsageDomain(node: ts.Identifier): UsageDomain | undefined { .api }Parameters:
node: ts.Identifier - The identifier node to analyzeReturns: The usage domain(s) for the identifier, or undefined if the context doesn't represent a usage
Example:
import { getUsageDomain, UsageDomain } from 'ts-api-utils';
function analyzeIdentifierContext(identifier: ts.Identifier) {
const domain = getUsageDomain(identifier);
if (!domain) {
console.log('Identifier is not a usage (e.g., in declaration position)');
return;
}
if (domain & UsageDomain.Type) {
console.log('Used in type context');
}
if (domain & UsageDomain.Value) {
console.log('Used in value context');
}
if (domain === UsageDomain.TypeQuery) {
console.log('Used in typeof expression');
}
}Determines which domain(s) an identifier declaration exists within based on its declaration context.
function getDeclarationDomain(node: ts.Identifier): DeclarationDomain | undefined { .api }Parameters:
node: ts.Identifier - The identifier node in a declaration positionReturns: The declaration domain(s) for the identifier, or undefined if not a declaration
Example:
import { getDeclarationDomain, DeclarationDomain } from 'ts-api-utils';
function analyzeDeclarationContext(identifier: ts.Identifier) {
const domain = getDeclarationDomain(identifier);
if (!domain) {
console.log('Identifier is not in declaration position');
return;
}
if (domain & DeclarationDomain.Type && domain & DeclarationDomain.Value) {
console.log('Declares both type and value (e.g., class or enum)');
} else if (domain & DeclarationDomain.Type) {
console.log('Type declaration (interface, type alias)');
} else if (domain & DeclarationDomain.Value) {
console.log('Value declaration (variable, function)');
}
if (domain & DeclarationDomain.Import) {
console.log('Import declaration');
}
}Understanding and working with TypeScript's scoping rules for accurate usage analysis.
The core interface for representing scopes in the usage analysis system.
interface Scope {
addUse(use: Usage, scope?: Scope): void;
addVariable(
identifier: string,
name: ts.PropertyName,
selector: ScopeBoundarySelector,
exported: boolean,
domain: DeclarationDomain
): void;
createOrReuseEnumScope(name: string, exported: boolean): EnumScope;
createOrReuseNamespaceScope(
name: string,
exported: boolean,
ambient: boolean,
hasExportStatement: boolean
): NamespaceScope;
end(cb: UsageInfoCallback): void;
getDestinationScope(selector: ScopeBoundarySelector): Scope;
getFunctionScope(): Scope;
getVariables(): Map<string, InternalUsageInfo>;
markExported(name: ts.ModuleExportName, as?: ts.ModuleExportName): void;
} { .api }Enumeration defining different types of scope boundaries in TypeScript.
enum ScopeBoundary {
None = 0,
Function = 1,
Block = 2,
Type = 4,
ConditionalType = 8
} { .api }Values:
None - No scope boundaryFunction - Function scope boundary (functions, methods, constructors)Block - Block scope boundary (blocks, loops, conditionals)Type - Type parameter scope boundaryConditionalType - Conditional type scope boundary (T extends U ? X : Y)Enumeration for selecting which scope boundaries to consider during analysis.
enum ScopeBoundarySelector {
Function = 1, // ScopeBoundary.Function
Block = 3, // Function | Block
InferType = 8, // ScopeBoundary.ConditionalType
Type = 7 // Block | Type
} { .api }Determines if a node represents a block scope boundary.
function isBlockScopeBoundary(node: ts.Node): ScopeBoundary { .api }Parameters:
node: ts.Node - The AST node to checkReturns: The type of scope boundary represented by the node
Example:
import { isBlockScopeBoundary, ScopeBoundary } from 'ts-api-utils';
function analyzeScope(node: ts.Node) {
const boundary = isBlockScopeBoundary(node);
if (boundary & ScopeBoundary.Function) {
console.log('Function scope boundary');
}
if (boundary & ScopeBoundary.Block) {
console.log('Block scope boundary');
}
if (boundary & ScopeBoundary.Type) {
console.log('Type parameter scope boundary');
}
}import { collectVariableUsage } from 'ts-api-utils';
function findUnusedVariables(sourceFile: ts.SourceFile): ts.Identifier[] {
const usage = collectVariableUsage(sourceFile);
const unused: ts.Identifier[] = [];
for (const [identifier, info] of usage) {
// Skip exported variables - they might be used externally
if (info.exported) continue;
// Skip global declarations - they might be used by other files
if (info.inGlobalScope) continue;
// Variable is unused if it has no usage references
if (info.uses.length === 0) {
unused.push(identifier);
}
}
return unused;
}import {
collectVariableUsage,
DeclarationDomain,
UsageDomain
} from 'ts-api-utils';
function analyzeCrossDomainUsage(sourceFile: ts.SourceFile) {
const usage = collectVariableUsage(sourceFile);
for (const [identifier, info] of usage) {
const name = identifier.getText();
// Find identifiers that exist in multiple domains
if (info.domain === DeclarationDomain.Any) {
console.log(`${name} exists in all domains (likely enum or class)`);
// Analyze how it's actually used
const typeUses = info.uses.filter(use => use.domain & UsageDomain.Type);
const valueUses = info.uses.filter(use => use.domain & UsageDomain.Value);
if (typeUses.length > 0 && valueUses.length > 0) {
console.log(` Used in both type and value contexts`);
} else if (typeUses.length > 0) {
console.log(` Only used in type contexts`);
} else if (valueUses.length > 0) {
console.log(` Only used in value contexts`);
}
}
}
}import { collectVariableUsage, DeclarationDomain } from 'ts-api-utils';
function canSafelyRename(
sourceFile: ts.SourceFile,
targetIdentifier: ts.Identifier,
newName: string
): boolean {
const usage = collectVariableUsage(sourceFile);
const targetInfo = usage.get(targetIdentifier);
if (!targetInfo) {
return false; // Identifier not found
}
// Check if new name conflicts with existing declarations
for (const [identifier, info] of usage) {
if (identifier.getText() === newName) {
// Check for domain conflicts
if (info.domain & targetInfo.domain) {
// Names would conflict in overlapping domains
return false;
}
}
}
return true;
}import {
collectVariableUsage,
DeclarationDomain,
UsageDomain
} from 'ts-api-utils';
function findTypeOnlyImports(sourceFile: ts.SourceFile): string[] {
const usage = collectVariableUsage(sourceFile);
const typeOnlyImports: string[] = [];
for (const [identifier, info] of usage) {
// Check if it's an import
if (!(info.domain & DeclarationDomain.Import)) continue;
// Check if all uses are in type contexts
const hasValueUsage = info.uses.some(use =>
use.domain & (UsageDomain.Value | UsageDomain.ValueOrNamespace)
);
if (!hasValueUsage && info.uses.length > 0) {
typeOnlyImports.push(identifier.getText());
}
}
return typeOnlyImports;
}TypeScript version compatibility utility for working with identifier keyword kinds.
function identifierToKeywordKind(node: ts.Identifier): ts.SyntaxKind | undefined;Parameters:
node: ts.Identifier - The identifier node to checkReturns: The syntax kind if the identifier represents a keyword, undefined otherwise
Description: This function provides compatibility support for TypeScript versions before 5.0 that don't have the built-in identifierToKeywordKind function.
Example:
import { identifierToKeywordKind } from 'ts-api-utils';
function isKeywordIdentifier(identifier: ts.Identifier): boolean {
return identifierToKeywordKind(identifier) !== undefined;
}Usage Analysis works seamlessly with other ts-api-utils modules for comprehensive code analysis.
import {
collectVariableUsage,
isVariableDeclaration,
hasModifiers
} from 'ts-api-utils';
function analyzeVariableModifiers(sourceFile: ts.SourceFile) {
const usage = collectVariableUsage(sourceFile);
for (const [identifier, info] of usage) {
// Check each declaration location
for (const declaration of info.declarations) {
const parent = declaration.parent;
if (isVariableDeclaration(parent)) {
const variableStatement = parent.parent?.parent;
if (hasModifiers(variableStatement)) {
console.log(`${identifier.getText()} has modifiers`);
}
}
}
}
}import {
collectVariableUsage,
getCallSignaturesOfType
} from 'ts-api-utils';
function analyzeCallableVariables(
sourceFile: ts.SourceFile,
typeChecker: ts.TypeChecker
) {
const usage = collectVariableUsage(sourceFile);
for (const [identifier, info] of usage) {
// Only analyze value declarations
if (!(info.domain & DeclarationDomain.Value)) continue;
const type = typeChecker.getTypeAtLocation(identifier);
const signatures = getCallSignaturesOfType(type);
if (signatures.length > 0) {
console.log(`${identifier.getText()} is callable with ${signatures.length} signature(s)`);
}
}
}// Safe usage information access
function getUsageInfo(
usage: Map<ts.Identifier, UsageInfo>,
identifier: ts.Identifier
): UsageInfo | undefined {
return usage.get(identifier);
}
// Domain-specific usage filtering
function getTypeUsages(info: UsageInfo): Usage[] {
return info.uses.filter(use => use.domain & UsageDomain.Type);
}
// Exported identifier detection
function isExported(info: UsageInfo): boolean {
return info.exported || info.inGlobalScope;
}The Usage Analysis module provides the foundation for sophisticated TypeScript code analysis, enabling tools to understand identifier lifecycles, detect patterns, and perform safe transformations while respecting TypeScript's complex scoping and domain rules.
Install with Tessl CLI
npx tessl i tessl/npm-ts-api-utilsevals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10