AgentSkillsCN

project-system

适用于 TypeScript 项目配置、tsconfig.json 文件、构建工具、打包工具以及编译流水线时使用。涵盖编译器选项、模块系统、项目引用以及声明文件等内容。 适用场景:tsconfig.json 配置、编译器选项、构建工具选型、打包工具搭建、单体仓库项目引用、路径别名、声明文件、模块解析 不适用场景:包管理器选型(请使用包管理相关技能)、CLI 工具开发(请使用 CLI 相关技能)、特定库的使用(请使用对应的库相关技能)

SKILL.md
--- frontmatter
name: project-system
description: |
  Use when configuring TypeScript projects, tsconfig.json, build tools, bundlers, and compilation pipelines. Covers compiler options, module systems, project references, and declaration files.
  USE FOR: tsconfig.json configuration, compiler options, build tool selection, bundler setup, monorepo project references, path aliases, declaration files, module resolution
  DO NOT USE FOR: package manager selection (use package-management), CLI tool development (use cli), specific library usage (use packages)
license: MIT
metadata:
  displayName: "TypeScript Project System"
  author: "Tyler-R-Kendrick"
compatibility: claude, copilot, cursor

TypeScript Project System

Overview

The TypeScript project system controls how TypeScript code is compiled, type-checked, and bundled. At its core is tsconfig.json, which defines compiler behavior, file inclusion, module resolution, and output configuration. This skill covers everything from basic compiler options to advanced monorepo setups with project references and modern bundler pipelines.

tsconfig.json Anatomy

A tsconfig.json file has four main sections:

jsonc
{
  // Compiler behavior — the bulk of configuration
  "compilerOptions": {
    "target": "ES2022",               // JavaScript version to emit
    "module": "NodeNext",             // Module system for output
    "moduleResolution": "NodeNext",   // How imports are resolved
    "strict": true,                   // Enable all strict type-checking
    "esModuleInterop": true,          // Allow default imports from CJS
    "declaration": true,              // Generate .d.ts files
    "declarationMap": true,           // Source maps for .d.ts files
    "sourceMap": true,                // Source maps for .js files
    "outDir": "./dist",              // Output directory
    "rootDir": "./src",              // Root of source files
    "baseUrl": ".",                   // Base for non-relative imports
    "paths": {                        // Path aliases
      "@/*": ["./src/*"]
    },
    "lib": ["ES2022"],               // Runtime library type definitions
    "isolatedModules": true,          // Ensure each file can be transpiled independently
    "verbatimModuleSyntax": true,     // Enforce explicit type-only imports
    "noUncheckedIndexedAccess": true, // Index signatures include undefined
    "skipLibCheck": true              // Skip .d.ts type checking for speed
  },

  // Files to include in compilation
  "include": ["src/**/*.ts", "src/**/*.tsx"],

  // Files to exclude from compilation
  "exclude": ["node_modules", "dist", "**/*.test.ts"],

  // Project references for monorepos
  "references": [
    { "path": "../shared" },
    { "path": "../utils" }
  ]
}

Key Compiler Options

OptionValues / TypePurpose
targetES5, ES2015..ES2024, ESNextJavaScript version for emitted code. Use ES2022 for modern Node.js, ES2015 for wide browser support.
moduleCommonJS, ES2015, ES2020, ES2022, Node16, NodeNext, PreserveModule system for output files. NodeNext for Node.js, Preserve for bundlers.
moduleResolutionnode, node16, nodenext, bundlerAlgorithm for resolving import specifiers. Must match module setting.
strictbooleanMaster switch for all strict checks. Always enable.
esModuleInteropbooleanEmit __importDefault helpers for CJS default imports. Required for most npm packages.
declarationbooleanGenerate .d.ts declaration files alongside .js output.
sourceMapbooleanGenerate .js.map files for debugger support.
pathsRecord<string, string[]>Path alias mappings (e.g., @/* to ./src/*). Requires baseUrl.
baseUrlstringBase directory for resolving non-relative module names.
outDirstringRedirect output to this directory.
rootDirstringSpecifies the root folder of source files (controls output directory structure).
libstring[]Which built-in API declarations to include (e.g., DOM, ES2022).
jsxpreserve, react, react-jsx, react-jsxdevJSX emit mode. Use react-jsx for React 17+ automatic runtime.
isolatedModulesbooleanEnsure each file can be safely transpiled by non-tsc tools (esbuild, swc).
verbatimModuleSyntaxbooleanRequire explicit import type for type-only imports. Replaces importsNotUsedAsValues.
noUncheckedIndexedAccessbooleanAdd undefined to index signature results, catching unsafe property access.
incrementalbooleanSave compilation state to .tsbuildinfo for faster rebuilds.
compositebooleanRequired for project references. Implies declaration and incremental.
resolveJsonModulebooleanAllow importing .json files as modules.
allowImportingTsExtensionsbooleanAllow .ts / .tsx in import specifiers (requires noEmit or emitDeclarationOnly).

Module Systems: CommonJS vs ESM

Comparison

AspectCommonJS (CJS)ES Modules (ESM)
Syntaxrequire() / module.exportsimport / export
LoadingSynchronousAsynchronous
Tree-shakingNot possibleSupported by bundlers
Top-level awaitNot supportedSupported
File extension.cjs or .js (default in Node).mjs or .js (with "type": "module")
__dirname / __filenameAvailableNot available (use import.meta.url)
InteropCan require() CJS, cannot require() ESMCan import both CJS and ESM

Node16 / NodeNext Resolution

When using "module": "NodeNext" and "moduleResolution": "NodeNext":

  • TypeScript follows Node.js native ESM resolution rules.
  • File extensions are required in relative imports: import { foo } from "./utils.js" (even though the source is .ts).
  • The package.json "type" field determines whether .js files are CJS or ESM.
  • Use "type": "module" for ESM-first projects.
  • Dual CJS/ESM packages use "exports" with conditional exports.

Bundler Resolution

When using "moduleResolution": "bundler":

  • Allows extensionless imports like import { foo } from "./utils".
  • Designed for projects processed by Vite, esbuild, webpack, or similar tools.
  • Does not require "type": "module" in package.json.
  • Best choice when you never run tsc for emit and use a bundler instead.

Build Tools Comparison

ToolSpeedTree-ShakingHMRConfig ComplexityType CheckingBest For
tscSlowNoNoLow (tsconfig)YesType checking, declaration generation
esbuildVery FastYesNo (needs plugin)LowNoFast builds, simple bundling
swcVery FastNo (alone)NoLowNoFast transpilation, Jest transform
ViteFastYes (Rollup)Yes (native)MediumNo (uses esbuild)Frontend apps, dev servers
RollupMediumYes (best)Plugin-basedMediumNoLibraries, tree-shaken bundles
webpackSlowYesYes (native)HighVia ts-loader/fork-ts-checkerComplex apps, legacy projects
tsupVery FastYesNoVery LowNo (uses esbuild)Library bundling, dual CJS/ESM
unbuildFastYes (Rollup)NoVery LowNoLibrary bundling, auto-config

Common Pattern: tsc for Types + Bundler for Output

Most projects use tsc only for type-checking (tsc --noEmit) and a faster tool for actual compilation:

bash
# Type-check only
tsc --noEmit

# Build with a bundler
vite build        # for apps
tsup src/index.ts # for libraries

Bundler Configuration Examples

Vite (Frontend Apps)

typescript
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [
    react(),
    tsconfigPaths(), // resolves tsconfig paths aliases
  ],
  build: {
    target: "ES2022",
    sourcemap: true,
  },
  server: {
    port: 3000,
    open: true,
  },
});

tsup (Library Bundling)

typescript
// tsup.config.ts
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["cjs", "esm"],      // dual output
  dts: true,                    // generate .d.ts
  sourcemap: true,
  clean: true,                  // clean dist/ before build
  splitting: false,             // no code splitting for libraries
  target: "ES2022",
  outDir: "dist",
  treeshake: true,
  // External dependencies (not bundled)
  external: ["react", "react-dom"],
});

unbuild (Auto-Configured Library Bundling)

typescript
// build.config.ts
import { defineBuildConfig } from "unbuild";

export default defineBuildConfig({
  entries: ["src/index"],
  declaration: true,
  rollup: {
    emitCJS: true,
  },
  externals: ["react"],
});

Project References for Monorepos

Project references allow TypeScript to understand multi-package monorepos and build them incrementally.

Setup

Each package needs composite: true in its tsconfig:

jsonc
// packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,        // required for project references
    "declaration": true,       // implied by composite
    "declarationMap": true,    // enables "go to definition" across packages
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*.ts"]
}

The root tsconfig references all packages:

jsonc
// tsconfig.json (root)
{
  "files": [],                  // root does not compile anything itself
  "references": [
    { "path": "packages/shared" },
    { "path": "packages/api" },
    { "path": "packages/web" }
  ]
}

Packages that depend on other packages add their own references:

jsonc
// packages/api/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [
    { "path": "../shared" }
  ],
  "include": ["src/**/*.ts"]
}

Building

bash
# Build all referenced projects in dependency order
tsc --build

# Build with verbose output
tsc --build --verbose

# Clean all build outputs
tsc --build --clean

# Force full rebuild
tsc --build --force

Path Aliases

Path aliases replace long relative imports (../../../utils) with clean absolute-style imports (@/utils).

tsconfig.json

jsonc
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@components/*": ["./src/components/*"],
      "@utils/*": ["./src/utils/*"]
    }
  }
}

Bundler Resolution

Path aliases in tsconfig.json only affect TypeScript's understanding. Bundlers need their own configuration or a plugin:

  • Vite: Use vite-tsconfig-paths plugin (reads tsconfig paths automatically).
  • webpack: Configure resolve.alias in webpack.config.js.
  • esbuild: Use esbuild-plugin-tsc or tsconfig-paths at runtime.
  • Jest: Configure moduleNameMapper in jest.config.ts.
  • tsup: Reads tsconfig paths automatically.
  • Node.js: Use --loader tsx or tsconfig-paths/register at runtime.

Declaration Files

Generation

jsonc
{
  "compilerOptions": {
    "declaration": true,         // emit .d.ts files
    "declarationMap": true,      // emit .d.ts.map for "go to source"
    "declarationDir": "./dist",  // where to put .d.ts files
    "emitDeclarationOnly": true  // only emit .d.ts, no .js (when bundler handles JS)
  }
}

Package Exports for Types

jsonc
// package.json
{
  "name": "my-lib",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  },
  "types": "./dist/index.d.ts",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs"
}

The "types" condition must come first in the exports map so TypeScript resolves it before the runtime entry.

Type Checking Strategies

CI Pipeline

bash
# Type-check without emitting — fastest way to validate types in CI
tsc --noEmit

# With incremental caching for faster re-runs
tsc --noEmit --incremental

Incremental Builds

jsonc
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./.tsbuildinfo"
  }
}

The .tsbuildinfo file caches compilation state. Subsequent runs skip unchanged files. Add it to .gitignore.

Watch Mode

bash
# Watch and type-check continuously during development
tsc --noEmit --watch

# Watch with project references
tsc --build --watch

Parallel Type Checking

For large projects, run type checking in parallel with the build:

bash
# In package.json scripts
{
  "scripts": {
    "build": "tsup src/index.ts",
    "typecheck": "tsc --noEmit",
    "ci": "npm run typecheck && npm run build && npm run test"
  }
}

Best Practices

  1. Always enable strict: true — it is the single most impactful compiler option. Never disable individual strict checks without a documented reason.

  2. Use isolatedModules: true when using any transpiler other than tsc (esbuild, swc, Babel). It ensures your code is compatible with single-file transpilation.

  3. Use verbatimModuleSyntax: true in new projects to enforce explicit import type declarations and prevent runtime import of type-only modules.

  4. Enable noUncheckedIndexedAccess to catch undefined access on objects with index signatures and arrays.

  5. Separate type-checking from building — use tsc --noEmit for type safety and a faster tool (esbuild, swc, Vite) for actual output.

  6. Use skipLibCheck: true to speed up compilation by not type-checking node_modules/.d.ts files. This is safe for most projects.

  7. Pin your TypeScript version in package.json to avoid surprises when new versions ship stricter checks.

  8. Use project references for monorepos to get incremental builds and clear dependency boundaries.

  9. Always generate declaration maps (declarationMap: true) so consumers can navigate to your source instead of compiled .d.ts files.

  10. Use "moduleResolution": "bundler" for frontend projects and "NodeNext" for Node.js libraries that will be consumed directly without a bundler.