惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
C
Cyber Attacks, Cyber Crime and Cyber Security
G
GRAHAM CLULEY
T
Tenable Blog
T
Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
Intezer
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
D
Darknet – Hacking Tools, Hacker News & Cyber Security
K
Kaspersky official blog
Security Latest
Security Latest
P
Privacy & Cybersecurity Law Blog
Google Online Security Blog
Google Online Security Blog
SecWiki News
SecWiki News
P
Palo Alto Networks Blog
TaoSecurity Blog
TaoSecurity Blog
Webroot Blog
Webroot Blog
Spread Privacy
Spread Privacy
O
OpenAI News
The Last Watchdog
The Last Watchdog
P
Proofpoint News Feed
C
Check Point Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
人人都是产品经理
人人都是产品经理
S
Security @ Cisco Blogs
Scott Helme
Scott Helme
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
月光博客
月光博客
S
Securelist
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
T
Troy Hunt's Blog
W
WeLiveSecurity
GbyAI
GbyAI
N
News | PayPal Newsroom
Y
Y Combinator Blog
C
Cisco Blogs
H
Help Net Security
The GitHub Blog
The GitHub Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 【当耐特】
Jina AI
Jina AI
MongoDB | Blog
MongoDB | Blog
P
Proofpoint News Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
云风的 BLOG
云风的 BLOG
小众软件
小众软件
N
News and Events Feed by Topic

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
Custom Static Analysis with the TypeScript Compiler API: When ESLint Rules Are Not Enough
The Practica · 2026-06-27 · via The Practical Developer

You have a 40-package monorepo. The team agreed that packages/shared should not import from packages/orders. Three months later someone accidentally imports an order type into the shared validation module. The build passes. The tests pass. The bug surfaces in production when a customer’s order triggers a circular dependency at runtime.

ESLint cannot express that rule. Not with import/no-restricted-paths (which only handles path patterns, not semantic knowledge of which package owns which symbol). Not with @typescript-eslint (which can parse the AST but not resolve module boundaries across a monorepo).

You need the TypeScript Compiler API.

The TypeScript compiler is not just tsc. It is a full program analysis toolkit: a parser, a type checker, a symbol resolver, and a program transformation engine. Most developers never touch it because the documentation assumes you already know what a TypeChecker is and why you would call getAliasedSymbol(). This post skips the academic tour and goes straight to the patterns that earned their keep in production monorepos.

What the compiler API gives you that ESLint does not

ESLint operates on a per-file AST. It sees the syntax of a single file, not the type system of the project. That means ESLint rules cannot answer questions like:

  • “Is this symbol exported from the package’s public API or is it an internal implementation detail?”
  • “Does this import create a circular dependency at the module level?”
  • “Is this function signature compatible with all call sites across the project?”

The compiler API answers all three because it builds a Program object that represents the full project: every source file, every type, every import resolution, and every symbol’s complete declaration chain.

The mental model: source files, types, and the checker

Three objects form the foundation of every analysis script.

import * as ts from 'typescript';

// 1. A Program: the compiler's internal representation of the whole project.
const program = ts.createProgram({
  rootNames: ['src/index.ts'],
  options: { strict: true, noEmit: true },
});

// 2. A TypeChecker: the engine that resolves types and symbols.
const checker = program.getTypeChecker();

// 3. SourceFile: one file in the project, parsed into an AST.
const sourceFile = program.getSourceFile('src/index.ts')!;

The TypeChecker is the most important object. It is what separates a compiler API script from a fancy grep. The checker answers questions about symbols (what a name refers to), types (what shape a value has), and signatures (what a function accepts and returns).

Pattern 1: Enforcing package boundary rules

Back to the problem that opened this post. You need a script that runs in CI and fails if packages/shared imports any symbol that is owned by packages/orders.

The naive approach is path-based: check that import paths do not contain packages/orders. That breaks when someone renames a directory or uses a path alias. The correct approach is semantic: resolve each import to its source file, then check which package that source file belongs to.

import * as ts from 'typescript';
import { readFileSync } from 'node:fs';
import { globSync } from 'fast-glob';

interface PackageMap {
  [sourceFile: string]: string; // "/abs/path/to/file.ts" -> "shared"
}

function buildPackageMap(): PackageMap {
  const map: PackageMap = {};
  // Assumes each package has a package.json with a "name" field
  // and that packages live under packages/<name>/
  const pkgDirs = globSync('packages/*', { onlyDirectories: true });
  for (const dir of pkgDirs) {
    const pkgJson = JSON.parse(readFileSync(`${dir}/package.json`, 'utf-8'));
    const name = pkgJson.name;
    const files = globSync(`${dir}/src/**/*.ts`, { ignore: ['**/*.test.ts', '**/*.spec.ts'] });
    for (const file of files) {
      map[file] = name;
    }
  }
  return map;
}

With the package map built, you walk every import declaration, resolve it to its source file, and compare package names.

function checkBoundaries(
  program: ts.Program,
  checker: ts.TypeChecker,
  packageMap: PackageMap,
): ts.Diagnostic[] {
  const diagnostics: ts.Diagnostic[] = [];

  for (const sourceFile of program.getSourceFiles()) {
    if (sourceFile.isDeclarationFile) continue;
    const currentPkg = packageMap[sourceFile.fileName];
    if (!currentPkg) continue;

    ts.forEachChild(sourceFile, function visit(node) {
      if (ts.isImportDeclaration(node)) {
        const resolved = resolveImportTarget(sourceFile, node, program);
        if (resolved && packageMap[resolved] && packageMap[resolved] !== currentPkg) {
          diagnostics.push(createError(
            sourceFile,
            node,
            `Cannot import from "${packageMap[resolved]}". Package "${currentPkg}" only imports from public APIs.`
          ));
        }
      }
      ts.forEachChild(node, visit);
    });
  }

  return diagnostics;
}

The resolution function uses the compiler’s module resolution, which handles path aliases, node_modules, and barrel files transparently.

function resolveImportTarget(
  sourceFile: ts.SourceFile,
  node: ts.ImportDeclaration,
  program: ts.Program,
): string | null {
  const moduleSpecifier = node.moduleSpecifier;
  if (!ts.isStringLiteral(moduleSpecifier)) return null;

  const resolved = program.getResolvedModuleWithFailedLookupLocations(
    moduleSpecifier.text,
    sourceFile.fileName,
  );
  if (!resolved || !resolved.resolvedModule) return null;
  return resolved.resolvedModule.resolvedFileName;
}

Run this in CI as a ts-node script. It fails the pipeline with the same error format as tsc --noEmit, so developers see boundary violations in the same place they see type errors.

Pattern 2: Detecting leaked internal exports

Most packages have a public API surface (what is exported from the barrel index.ts) and internal implementation details (modules under src/internal/ or prefixed with _). Your team agreed that only the barrel exports are fair game for other packages to import.

The problem: TypeScript does not enforce this at compile time. A developer in another package can write import { InternalParser } from '@myapp/shared/src/internal/parser' and TypeScript will happily resolve it if src/internal/parser.ts is on the file system.

ESLint’s no-restricted-imports can block path patterns like **/internal/**, but it does not understand the project’s actual export map. Someone renames the directory to src/private/ and the rule is silent until you update it.

The compiler API approach: walk every export declaration in every file, mark which exports are “public” (reachable through the package’s entry point) and which are “internal” (exported but not re-exported from the barrel). Then flag any import of an internal symbol by a consumer outside the package.

function findInternalExports(program: ts.Program, entryPoint: string): Set<string> {
  // Find all symbols that are exported from the package's entry point.
  const entrySource = program.getSourceFile(entryPoint)!;
  const publicSymbols = collectExportedSymbols(entrySource);

  // Find all symbols that are exported anywhere in the package.
  const allExports = new Set<string>();
  for (const sf of program.getSourceFiles()) {
    if (sf.fileName.includes('node_modules')) continue;
    if (sf.isDeclarationFile) continue;
    ts.forEachChild(sf, (node) => {
      if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
        // Re-exports from another module - they bring symbols in
      } else if (isExportedDeclaration(node)) {
        allExports.add(`${sf.fileName}:${getName(node)}`);
      }
    });
  }

  // Internal = exported but not reachable from the entry point.
  const internal = new Set(allExports);
  for (const sym of publicSymbols) internal.delete(sym);
  return internal;
}

function collectExportedSymbols(sourceFile: ts.SourceFile): Set<string> {
  const symbols = new Set<string>();
  ts.forEachChild(sourceFile, (node) => {
    if (ts.isExportDeclaration(node)) {
      // export { X } from './foo' - walk the target file
      // export * from './foo' - walk the target file
      // For simplicity here, we handle named re-exports.
    } else if (isExportedDeclaration(node)) {
      symbols.add(`${sourceFile.fileName}:${getName(node)}`);
    }
  });
  return symbols;
}

Then the rule is simple: when an import resolves to a file and symbol that is in the internal set, and the importer is in a different package, emit a diagnostic.

This catches leaks that no path-based rule can see. A developer re-exports an internal type through a barrel deep in the directory tree, and the path from the entry point to that type breaks? The rule catches it because the internal set is computed from the actual export graph, not from a directory convention.

Pattern 3: Automated migration scripts

The most practical use of the compiler API is not linting. It is refactoring.

You renamed the database client from DbClient to DatabaseClient. The rename touches 200 files across 15 packages. A find-and-replace across the monorepo will miss cases where the type is imported under an alias, or where JSDoc references the old name.

A compiler API migration script handles all of these correctly because it operates on the type-checked AST, not on text.

import * as ts from 'typescript';
import * as fs from 'node:fs';
import { globSync } from 'fast-glob';

function renameSymbol(
  rootDir: string,
  oldName: string,
  newName: string,
): void {
  const fileNames = globSync(`${rootDir}/**/*.ts`, {
    ignore: ['**/node_modules/**'],
  });

  const program = ts.createProgram(fileNames, {
    noEmit: true,
    strict: true,
  });

  const checker = program.getTypeChecker();

  for (const sourceFile of program.getSourceFiles()) {
    if (sourceFile.isDeclarationFile) continue;
    if (!sourceFile.fileName.startsWith(rootDir)) continue;

    const updates: ts.TextChange[] = [];
    visitForRename(sourceFile, sourceFile, checker, oldName, newName, updates);
    if (updates.length === 0) continue;

    // Apply changes in reverse order to preserve offsets.
    let text = sourceFile.text;
    updates.sort((a, b) => b.span.start - a.span.start);
    for (const change of updates) {
      text =
        text.slice(0, change.span.start) +
        change.newText +
        text.slice(change.span.start + change.span.length);
    }
    fs.writeFileSync(sourceFile.fileName, text, 'utf-8');
  }
}

function visitForRename(
  node: ts.Node,
  sourceFile: ts.SourceFile,
  checker: ts.TypeChecker,
  oldName: string,
  newName: string,
  updates: ts.TextChange[],
): void {
  // Only rename identifier nodes.
  if (ts.isIdentifier(node) && node.text === oldName) {
    const symbol = checker.getSymbolAtLocation(node);
    if (symbol && shouldRename(symbol)) {
      updates.push({
        span: { start: node.getStart(sourceFile), length: node.getWidth(sourceFile) },
        newText: newName,
      });
    }
  }
  ts.forEachChild(node, (child) =>
    visitForRename(child, sourceFile, checker, oldName, newName, updates),
  );
}

The shouldRename function is where the type-aware filtering happens. You can check whether the symbol is a type, a value, a function, a class, or only certain declarations. This is how you rename only the class DbClient and not a local variable that happens to share the name.

function shouldRename(symbol: ts.Symbol): boolean {
  const declarations = symbol.declarations;
  if (!declarations || declarations.length === 0) return false;

  const decl = declarations[0];
  // Only rename class declarations, not variables or parameters that
  // happen to have the same name.
  if (!ts.isClassDeclaration(decl)) return false;

  // Skip if the original declaration is in a different package.
  // This prevents renaming a symbol defined in node_modules.
  if (decl.getSourceFile().fileName.includes('node_modules')) return false;

  return true;
}

The difference between this and a global sed: the compiler script skips identifiers in string literals, comments, and dead code paths that sed would blindly change. It also handles aliases: if someone wrote import { DbClient as DB } from './db', the script renames only the original declaration and the alias usage resolves through the symbol table.

Pattern 4: Finding dead code with cross-file reachability

TypeScript’s noUnusedLocals flag catches unused variables within a single file. It does not catch exported functions that no other module imports. For that you need a cross-file reachability analysis.

function findUnusedExports(program: ts.Program, checker: ts.TypeChecker): ts.Symbol[] {
  const reachable = new Set<ts.Symbol>();
  const exported = new Map<string, ts.Symbol>();

  // Collect all exported symbols.
  for (const sourceFile of program.getSourceFiles()) {
    if (sourceFile.isDeclarationFile) continue;
    ts.forEachChild(sourceFile, (node) => {
      if (isExportedDeclaration(node) && ts.isNamedDeclaration(node)) {
        const symbol = checker.getSymbolAtLocation(node.name!);
        if (symbol) {
          exported.set(
            `${sourceFile.fileName}:${symbol.getName()}`,
            symbol,
          );
        }
      }
    });
  }

  // Walk all import declarations and mark the referenced symbols.
  for (const sourceFile of program.getSourceFiles()) {
    if (sourceFile.isDeclarationFile) continue;
    ts.forEachChild(sourceFile, (node) => {
      if (ts.isImportDeclaration(node)) {
        const importClause = node.importClause;
        if (!importClause) return;
        if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
          for (const element of importClause.namedBindings.elements) {
            const symbol = checker.getSymbolAtLocation(element.name);
            if (symbol) reachable.add(symbol);
          }
        }
      }
    });
  }

  // Unused = exported but never imported.
  const unused: ts.Symbol[] = [];
  for (const [key, symbol] of exported) {
    if (!reachable.has(symbol)) {
      unused.push(symbol);
    }
  }
  return unused;
}

This is a simplified version. A production script also handles dynamic imports (import()), re-exports (export * from), and barrel files. But even the simplified version catches cases that noUnusedLocals misses, like a utility function that was exported “just in case” three refactors ago and now nobody calls it.

Where to run these scripts

None of these should run during tsc --noEmit in every developer’s workflow. The compiler API scripts are slower than ESLint (building the full program takes 1-3 seconds even for medium monorepos), so they belong in CI, not in your editor’s save hook.

Add a dedicated npm script:

{
  "scripts": {
    "check:boundaries": "tsx scripts/check-boundaries.ts"
  }
}

Run it in CI alongside type checking, not in place of it. The boundary check and dead-export check are complementary to tsc --noEmit, not replacements.

For the rename and migration scripts, run them once and commit the result. Do not wire them into CI. They are developer tools, not pipeline checks.

Putting it all together

The TypeScript Compiler API is the tool you reach for when path-based rules cannot express the constraint you need. The four patterns covered here enforce real constraints that come up in every medium-to-large TypeScript codebase:

  • Package boundary rules prevent circular dependencies and unauthorized coupling.
  • Internal export detection enforces a public API surface that your package.json exports field cannot capture alone.
  • Rename and migration scripts handle cross-package refactors correctly, with type awareness that sed and grep cannot match.
  • Dead export detection finds code that noUnusedLocals misses because the unused symbol happens to have an export keyword in front of it.

Each pattern builds on the same three objects: Program, SourceFile, and TypeChecker. Once you understand how those three interact, the compiler API becomes a general-purpose toolkit for analyzing and transforming TypeScript code. The patterns in this post are a starting point, not an exhaustive catalog. The same approach works for dependency graphs, API surface diffing, custom code generation, and migration verification.


A note from Yojji

The kind of tooling discipline that builds custom analysis scripts to enforce package boundaries and catch dead code before it ships is exactly the kind of engineering maturity that separates projects that age well from projects that accumulate technical debt with every commit. Yojji builds and maintains TypeScript codebases where these patterns are standard practice, not aspirational goals. Yojji is an international custom software development company founded in 2016, with teams across Europe, the US, and the UK. Their engineers work deeply in the JavaScript and TypeScript ecosystem, building full-cycle products from discovery through deployment, including the kind of pragmatic tooling investments that compound into reliable, maintainable systems.