---
title: TypeScript 多包配置：类型检查与运行环境的分界
description: 核对 ChatCrystal 的共享类型、服务端、Vite 客户端和 Electron 配置，用可复现的别名反例解释编译通过为何不等于运行成功。
image: "https://zengliangyi.com/og/blog/typescript-monorepo-configuration.png"
---

一个代码仓库里的 TypeScript 文件可能由完全不同的程序执行：Node 服务端、Vite 浏览器构建和 Electron 主进程。共享类型不代表共享所有编译选项。本文检查 ChatCrystal 的实际配置继承、输出目录和模块解析，再用一个临时项目证明路径别名的运行边界。

## 先画出真实配置关系

当前 [基础配置](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/tsconfig.base.json)设置 ES2022、ESNext 模块、bundler 解析、strict 和 isolatedModules 等。只有直接或间接 extends 的配置继承它；不能看到根目录有 base 文件就宣布所有子项目都使用同一配置。

| 范围 | 实际配置关系 | 主要运行边界 |

| --- | --- | --- |

| shared | extends 基础配置，composite 为 true | 包入口指向 types/index.ts，共享类型契约 |

| server | extends 基础配置，rootDir 为上级目录 | tsc 输出 JS，生产由 Node 执行 |

| client | 根配置引用 app 与 node，两者不 extends 基础配置 | app 面向浏览器，node 面向 Vite 配置；均 noEmit |

| electron | 独立配置，module 与解析均为 Node16 | 主进程编译输出，再由打包流程使用 |

[根 package.json](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/package.json)的工作区是 shared、server、client，electron 不在这份工作区清单里。客户端的项目引用指向 app 和 node，不是引用整个 shared 项目。shared 的 composite 也不会自动让所有消费者形成一个 project references 构建图。

## 服务端开发执行与生产输出分开看

[server/tsconfig.json](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/tsconfig.json)同时包含 src 和上级 shared/types，因此 rootDir 是上级目录，outDir 是 server/dist。由此形成 dist/server/src 等路径，不是简单把 src 平移为 dist。

[服务端脚本](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/package.json)开发时使用 tsx watch；生产构建先清理 dist，再 tsc，最后复制种子数据，启动入口是 dist/server/src/index.js。仅运行 tsc 不能替代整条构建命令。JSON 等运行资源也不能假定总会由类型编译器自动放到正确位置。

server 包声明 type:module；基础 module:ESNext 控制发出语法，包的 type 与 Node 解析共同决定如何执行文件。开发时能被 tsx 加载的路径，仍应在生产输出中验证，不能只以开发服务器能启动作为验收。

## 客户端别名同时存在于两层

[客户端 app 配置](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/tsconfig.app.json)使用 ES2023、DOM 类型、react-jsx、bundler 解析和 noEmit。Vite 配置的 [node 项目](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/tsconfig.node.json)使用 Node 类型，不是把 DOM 配置整份复制过去。两者还包含 verbatimModuleSyntax 等检查；只看旧版 base 配置会漏掉这些实际选项。

`@/*` 在 TypeScript paths 中映射到 src，在 [vite.config.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/vite.config.ts)中另有运行构建别名，使用 import.meta.dirname 定位路径。类型解析和打包解析分别有配置，不能只修改其中一处就认为另一处跟随。paths 的含义见[官方说明](https://www.typescriptlang.org/tsconfig/paths.html)：它不会改写发出的 import 路径。

## 用临时项目验证别名反例

客户端将 typescript 指向 @typescript/typescript6 6.0.2 包装器，其底层依赖在[锁文件](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/pnpm-lock.yaml)中解析为真正的 TypeScript 6.0.3。下面直接安装这个检查器版本，避免把包装器版本当作编译器版本。练习**不运行 ChatCrystal 的构建或发布脚本**；在新目录创建以下文件，用 Node 24+ 执行。

`package.json` 声明教学项目使用 ESM：



```json
{   "private"  :   true  ,   "type"  :   "module"   }
```



安装固定版本的编译器：



```sh
npm   install   --ignore-scripts   --no-audit   --no-fund   typescript@6.0.3
```



保存 `tsconfig.json`。rootDir 显式决定输出布局；这个小项目自行发出 JS，与客户端的 noEmit 配置不同。



```json
{  
    "compilerOptions"  : {  
      "target"  :   "ES2022"  ,  
      "module"  :   "ESNext"  ,  
      "moduleResolution"  :   "bundler"  ,  
      "strict"  :   true  ,  
      "rootDir"  :   "src"  ,  
      "outDir"  :   "dist"  ,  
      "paths"  : {   "@lesson/*"  : [  "./src/*"  ] }  
    },  
    "include"  : [  "src"  ]  
  }
```



创建 src 目录，保存 `src/values.ts`：



```ts
export   const   value   =   42  ;
```



保存 `src/main.ts`，先保留刻意存在的别名：



```ts
import   { value }   from   '@lesson/values.js'  ;  
  console.  log  (value);
```



先编译，再运行：



```sh
node   node_modules/typescript/bin/tsc   --project   tsconfig.json  
  node   dist/main.js
```



编译应通过，发出的 main.js 仍保留 @lesson/values.js。普通 Node 不认识这个自定义别名，第二条命令预期以 ERR_MODULE_NOT_FOUND 失败。这里没有配置打包器、加载器或 import 映射。

现在把 src/main.ts 的导入改为下面的相对路径，重新执行同样两条命令，应编译通过并输出 42：



```ts
import   { value }   from   './values.js'  ;  
  console.  log  (value);
```



先失败再恢复的证据说明的是路径解析边界，不是证明相对导入是所有项目的唯一方案。也可以使用构建器或运行环境支持的映射，但需要分别配置并验证。

## Electron 的 Node16 模式不是一个通用 CommonJS 开关

[electron/tsconfig.json](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/electron/tsconfig.json)使用 Node16 模块和解析，带 Node 与 Electron 类型、strict、sourceMap 等。Node16 会结合扩展名和包作用域决定模块形式。当前 electron 下没有自己的 package.json，根包也没有 type:module，所以现有普通 .ts 主进程文件处于 CommonJS 包作用域；不能把这个观察概括成 Node16 永远只发出 CommonJS。

新增选项前先问：谁检查类型，谁发出文件，谁处理路径，谁在生产执行？然后核对共享类型、资源复制和真实启动入口。strict 能发现部分类型错误，不能验证外部 JSON、文件存在或整个打包生命周期。本文只运行临时反例，没有执行应用打包或声称更改这些配置后必然成功。

分享：

##  相关文章

[##  五种 AI 对话解析器：字段保留与损失边界

从 Claude Code、Codex、Cursor、Trae 和 Copilot 的实际解析器建立验收矩阵，检查工具标记、思考文本、顺序与快照覆盖范围。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#TypeScript

#Node.js](/blog/ai-conversation-parser-boundaries)    [##  AI SDK Provider 抽象：能力表与协议边界

核对 ChatCrystal 的七项 Provider 注册、AI SDK 7 模型工厂和结构化输出，用无网络 mock 验证 Responses、Chat Completions、Embedding 与 schema 失败。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#TypeScript](/blog/ai-sdk-provider-abstraction)    [##  SourceAdapter 内部链路：身份、内容哈希与重新导入

对照 ChatCrystal 的注册表、导入器和远端载荷，验证会话身份、内容哈希、元数据捷径与 parserVersion 的实际边界。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#TypeScript

#Node.js](/blog/source-adapter-internals)

##  从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录，欢迎交流实现细节。

[访问我的 GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "TypeScript 多包配置：类型检查与运行环境的分界",
  "description": "核对 ChatCrystal 的共享类型、服务端、Vite 客户端和 Electron 配置，用可复现的别名反例解释编译通过为何不等于运行成功。",
  "url": "https://zengliangyi.com/blog/typescript-monorepo-configuration/",
  "image": "https://zengliangyi.com/images/blog/typescript-monorepo-configuration.svg",
  "datePublished": "2026-10-06T00:00:00.000Z",
  "dateModified": "2026-10-06T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/typescript-monorepo-configuration/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "TypeScript 多包配置：类型检查与运行环境的分界",
      "item": "https://zengliangyi.com/blog/typescript-monorepo-configuration/"
    }
  ]
}
```
