CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ts-api-utils

Utility functions for working with TypeScript's API, providing comprehensive tools for analyzing and manipulating TypeScript AST nodes, types, and compiler APIs.

79

1.97x
Overview
Eval results
Files

usage-analysis.mddocs/

Usage Analysis

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.

Overview

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:

  1. Track variable declarations across different syntactic contexts
  2. Monitor usage patterns for identifiers in type and value spaces
  3. Analyze scope boundaries and their effects on identifier visibility
  4. Distinguish between domains where identifiers exist (namespace, type, value spaces)

This capability is particularly valuable for tools that need to understand identifier lifecycles, detect unused variables, analyze dependencies, or perform safe refactoring operations.

Core Concepts

Domains

TypeScript operates with multiple "domains" or "spaces" where identifiers can exist:

  • Value Space: Runtime values, variables, functions
  • Type Space: Types, interfaces, type aliases
  • Namespace Space: Module namespaces and namespace declarations
  • Import Domain: Special handling for imported identifiers

Understanding these domains is crucial because the same identifier name can exist in different domains simultaneously without conflict.

Usage Patterns

The system tracks two primary aspects of identifier usage:

  1. Declarations: Where and how identifiers are introduced into scope
  2. References: Where identifiers are accessed or used

Scope Analysis

Variable usage is analyzed within the context of TypeScript's scoping rules, including:

  • Function scope boundaries
  • Block scope boundaries
  • Type parameter scopes
  • Conditional type scopes

Usage Collection

The main entry point for variable usage analysis.

collectVariableUsage

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 analyze

Returns: 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}`);
}

Usage Information Types

Data structures that represent how variables and types are used throughout code.

UsageInfo

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 declared
  • domain: DeclarationDomain - Which domain(s) the identifier exists in
  • exported: boolean - Whether the identifier was exported from its module/namespace scope
  • inGlobalScope: boolean - Whether any declaration was in the global scope
  • uses: Usage[] - All references to the identifier throughout the file

Usage

Represents 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 in
  • location: ts.Identifier - The AST node representing the usage location

DeclarationDomain

Enumeration 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 identifiers

UsageDomain

Enumeration 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 reference
  • Type - Used in a type position
  • Value - Used as a runtime value
  • Any - Could be used in any domain
  • TypeQuery - Used in typeof expressions
  • ValueOrNamespace - Ambiguous value or namespace usage

Example:

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`);
  }
}

Domain Analysis

Functions for understanding the context and domain of identifier usage.

getUsageDomain

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 analyze

Returns: 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');
  }
}

getDeclarationDomain

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 position

Returns: 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');
  }
}

Scope Analysis

Understanding and working with TypeScript's scoping rules for accurate usage analysis.

Scope Interface

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 }

ScopeBoundary

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 boundary
  • Function - Function scope boundary (functions, methods, constructors)
  • Block - Block scope boundary (blocks, loops, conditionals)
  • Type - Type parameter scope boundary
  • ConditionalType - Conditional type scope boundary (T extends U ? X : Y)

ScopeBoundarySelector

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 }

isBlockScopeBoundary

Determines if a node represents a block scope boundary.

function isBlockScopeBoundary(node: ts.Node): ScopeBoundary { .api }

Parameters:

  • node: ts.Node - The AST node to check

Returns: 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');
  }
}

Advanced Usage Patterns

Detecting Unused Variables

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;
}

Analyzing Cross-Domain Usage

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`);  
      }
    }
  }
}

Scope-Aware Refactoring

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;
}

Type-Only Import Detection

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;
}

Utility Functions

identifierToKeywordKind

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 check

Returns: 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;
}

Integration with Other Modules

Usage Analysis works seamlessly with other ts-api-utils modules for comprehensive code analysis.

With Node 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`);
        }
      }
    }
  }
}

With Type System Analysis

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)`);
    }
  }
}

Best Practices

Performance Considerations

  1. Cache results: Usage analysis is expensive - cache results when analyzing multiple files
  2. Scope appropriately: Only analyze files that need usage information
  3. Filter early: Use domain filters to focus on relevant identifiers

Accuracy Guidelines

  1. Consider all domains: Remember that identifiers can exist in multiple domains simultaneously
  2. Handle imports specially: Import declarations have special domain semantics
  3. Respect scope boundaries: Usage analysis respects TypeScript's scoping rules

Common Patterns

// 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-utils

docs

compiler-options.md

index.md

node-analysis.md

syntax-utilities.md

type-system.md

usage-analysis.md

tile.json