Configuring TypeScript Path Resolution with tsconfig.json Aliases

The code-review-graph repository resolves TypeScript path aliases by walking up the directory tree to locate tsconfig.json, parsing the paths configuration, and mapping aliased imports to absolute file paths using pattern matching and filesystem probing.

The code-review-graph tool parses JavaScript and TypeScript projects to build dependency graphs, but path aliases like @/utils require special handling to resolve correctly. When the parser encounters a non-relative import, it delegates resolution to a dedicated TypeScript configuration resolver that translates these aliases into concrete file system paths according to the rules defined in your tsconfig.json.

How the TypeScript Resolver Works

The resolution logic lives in code_review_graph/tsconfig_resolver.py and executes in five distinct phases to handle the complexity of TypeScript's module resolution strategy.

Loading the Nearest Configuration

The process begins with _load_tsconfig_for_file, which walks up the directory tree from the importing file until it discovers a tsconfig.json or tsconfig.app.json. The resolver caches these lookups per directory to avoid redundant filesystem operations on subsequent imports.

Parsing and Extending Configurations

Once located, the _parse_tsconfig method reads the JSON (including support for JSONC comments) and recursively resolves any extends chain via _resolve_extends. This ensures that inherited baseUrl and paths settings from parent configurations merge correctly with local overrides.

Extracting Resolution Parameters

After merging inherited options, the resolver extracts baseUrl and the alias map from paths. These parameters define the root directory for resolution and the pattern mappings that translate alias prefixes to relative subdirectories.

Pattern Matching and Resolution

The public entry point resolve_alias(import_str, file_path) receives the raw import string and the absolute path of the file containing it. It combines the tsconfig directory with baseUrl to form the absolute base directory, then matches the import against each pattern in the paths map.

Filesystem Probing

For each candidate path produced by pattern matching, _match_and_probe checks a list of extensions in priority order: .ts, .tsx, .js, .jsx, and .vue. The resolver returns the first existing file it finds, or None if all candidates fail, leaving the import unresolved in the final graph.

Integration with the Generic Parser

The parser in code_review_graph/parser.py invokes the resolver whenever it encounters non-relative imports in JavaScript or TypeScript files. The integration point appears in the _do_resolve_module method:


# code_review_graph/parser.py – lines 1334-1360

elif language in ("javascript", "typescript", "tsx", "vue"):
    if module.startswith("."):
        # …relative handling…

    else:
        # Non‑relative import — try tsconfig path alias resolution

        resolved = self._tsconfig_resolver.resolve_alias(module, file_path)
        if resolved:
            return resolved

When the resolver returns a valid path, the graph creates an IMPORTS_FROM edge pointing to that specific file. If resolution fails, the edge points to the raw module name, preserving the import relationship without filesystem validation.

Setting Up tsconfig.json Aliases

To enable alias resolution in your project, configure the compilerOptions section of your TypeScript configuration:

  1. Create or edit tsconfig.json (or tsconfig.app.json) at the project root or in an ancestor directory of your source files.

  2. Define baseUrl and paths:

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@/utils/*": ["utils/*"],
      "@/components/*": ["components/*"]
    }
  }
}
  1. Ensure patterns end with /* to match the resolver's glob implementation, which strips the wildcard and appends the corresponding portion of the import path.

Example: Resolving an Alias in Practice

Consider a project where src/app/main.ts contains the following imports:

import { foo } from "@/utils/foo";
import { Bar } from "@/components/bar";

When parser.py processes this file, it invokes:

self._tsconfig_resolver.resolve_alias("@/utils/foo", "/abs/path/src/app/main.ts")

The resolver executes the following sequence:

Upon finding the file, the graph records an IMPORTS_FROM edge from main.ts to utils/foo.ts, enabling accurate dependency analysis and blast-radius calculations.

Testing the Implementation

The repository includes a comprehensive test suite to verify alias resolution behavior. Run the tests using pytest:

pytest tests/test_tsconfig_resolver.py

Key test cases include:

  • test_resolve_alias – Verifies successful resolution of valid aliases like @/lib/utils
  • test_resolve_alias_nonexistent_returns_none – Confirms None is returned for missing target files
  • test_resolve_npm_package_returns_none – Ensures npm package names (e.g., react) are not treated as path aliases
  • test_no_tsconfig_returns_none – Validates graceful handling when no configuration file exists in the directory tree
  • test_caching – Confirms that repeated lookups for the same directory use cached results

Summary

  • Resolution starts in code_review_graph/tsconfig_resolver.py, which handles the complete lifecycle from configuration discovery to filesystem probing.
  • _load_tsconfig_for_file walks up the directory tree and caches results, while _resolve_extends handles configuration inheritance with JSONC support.
  • The resolve_alias method matches import strings against paths patterns and checks multiple extensions (.ts, .tsx, .js, .jsx, .vue) to locate the target file.
  • code_review_graph/parser.py automatically invokes the resolver for all non-relative imports in JavaScript, TypeScript, TSX, and Vue files.
  • Proper configuration requires baseUrl and paths entries ending with /* wildcards to enable correct pattern substitution.

Frequently Asked Questions

What happens if no tsconfig.json file exists?

The resolver returns None immediately, and the parser records the import using the raw module string rather than a filesystem path. This behavior ensures the graph builds successfully even for projects without TypeScript configuration, though dependency edges will lack precise file resolution.

Does the resolver support monorepos with multiple tsconfig files?

Yes. The _load_tsconfig_for_file method locates the nearest configuration by walking upward from each importing file, allowing different packages in a monorepo to use distinct baseUrl and paths settings. The directory-level cache ensures that files within the same directory reuse the same configuration instance.

How does the resolver distinguish between npm packages and path aliases?

The resolver attempts resolution for all non-relative imports, but npm packages fail the pattern matching phase because they do not match any keys in the paths map. Consequently, test_resolve_npm_package_returns_none verifies that imports like react or lodash correctly return None, allowing the parser to treat them as external dependencies rather than internal aliases.

Why does my alias resolve to None despite correct configuration?

Common causes include missing wildcard suffixes in the paths entry (the resolver requires /* to match dynamic path segments), incorrect baseUrl relative to the tsconfig.json location, or missing file extensions. The resolver probes for .ts, .tsx, .js, .jsx, and .vue files, but if none exist at the constructed path, it returns None as implemented in _match_and_probe.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →