跳到主要内容

TypeScript 项目案例与配置

这一篇回到当前仓库,专门看 TypeScript 在项目里到底是怎么落地的。

它不是讲语法,而是把“配置写在哪、类型从哪来、实际代码怎么写”串起来。你可以把它当成一篇项目内导读。

先看入口:这个项目的 TypeScript 配置在哪

这个站点本身就是一个 Docusaurus + React + TypeScript 项目,最先值得看的几个文件是:

  • tsconfig.json
  • package.json
  • docusaurus.config.ts

从这三个文件可以先看出几件事:

  • 项目已经接入了 typescript
  • 有单独的 typecheck 脚本:tsc
  • tsconfig.json 继承了 @docusaurus/tsconfig
  • 这里的 TS 配置偏“站点工程配置”,而不是从零自建一套完整前端工程配置

案例 1:站点配置文件怎么写类型

docusaurus.config.ts 里,有两个很典型的写法:

import type { Config } from "@docusaurus/types";
import type * as Preset from "@docusaurus/preset-classic";

以及:

const config: Config = {
presets: [
[
"classic",
{
docs: {
sidebarPath: require.resolve("./sidebars.js"),
},
} satisfies Preset.Options,
],
],
};

这里值得注意的是两点:

  • import type 只引入类型,不引入运行时值
  • satisfies 很适合配置对象,能校验结构,又不会像强制断言那样把错误压掉

如果你在项目里经常写路由配置、菜单配置、表单 schema、构建配置、UI token 配置,satisfies 基本都值得优先考虑。

案例 2:首页里的对象数组怎么建类型

src/pages/index.tsx 里定义了:

type Principle = {
title: string;
description: string;
};

type PlanItem = {
level: string;
title: string;
tags: string[];
time: string;
};

然后再配合:

const principles: Principle[] = [/* ... */];
const planItems: PlanItem[] = [/* ... */];

这类写法在内容型项目、后台项目和中后台配置页里都非常常见,因为它把“内容数据”和“渲染逻辑”拆开了。

如果字段值是固定集合,还可以继续收紧,比如把:

level: string;

收成:

level: "高优先" | "中优先" | "低优先";

案例 3:React 组件返回值和 Props 类型

src/components/CommonLayout/index.tsx 里有这种写法:

const CommonLayout = ({ children }: { children: ReactNode }): JSX.Element => {
// ...
};

这里比较值得总结的是:

  • 很短的组件可以先内联写 Props
  • 一旦组件开始复用,最好把 Props 单独命名成类型
  • 返回值类型可以显式写,也可以交给 TypeScript 推断,重点是对外接口要稳定

更适合扩展的版本通常会写成:

type CommonLayoutProps = {
children: ReactNode;
};

const CommonLayout = ({ children }: CommonLayoutProps): JSX.Element => {
// ...
};

案例 4:主题覆写时直接复用框架提供的类型

src/theme/DocRoot/Layout/Sidebar/index.tsx 里,有这类写法:

import type { Props } from "@theme/DocRoot/Layout/Sidebar";

然后:

export default function DocRootLayoutSidebar({
sidebar,
hiddenSidebarContainer,
setHiddenSidebarContainer,
}: Props): ReactNode {
// ...
}

这体现了一个很稳的项目实践:如果上游框架已经暴露了组件 Props 类型,就优先直接复用,不要自己重写一个“差不多”的版本。

案例 5:Record 很适合做“名称到处理函数”的映射

src/theme/prism-include-languages.ts 里有一段很实用:

const languageLoaders: Record<string, () => void> = {
bash: () => {
require("prismjs/components/prism-bash.js");
},
powershell: () => {
require("prismjs/components/prism-powershell.js");
},
};

这就是典型的“字符串键 -> 处理逻辑”的映射对象。状态到文案、文件类型到图标、命令名到处理函数,本质上都是这一类问题。

如果键的集合是固定的,还可以进一步收紧成联合类型键,而不是直接用 string

案例 6:什么时候该用框架预设,什么时候自己补配置

tsconfig.json 现在的写法比较轻:

{
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"jsx": "react",
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["node"],
"baseUrl": "."
}
}

这说明这个项目的策略不是“从零完全自定义 tsconfig”,而是:

  1. 先继承框架推荐配置
  2. 再只补当前项目真正需要的几项

这通常比自己硬拼一整套配置更稳。

案例 7:环境变量在站点配置里怎么进入 TypeScript 代码

虽然这个仓库当前还没有单独的 env.d.ts,但 docusaurus.config.ts 顶部已经有一个很典型的入口:

require("dotenv").config();

这类配置文件往往就是环境变量最早进入项目的地方,比如:

  • 站点 URL
  • 第三方服务 key
  • 构建期开关
  • 搜索、监控、分析脚本配置

如果后面这个项目开始系统使用环境变量,更推荐补一层集中声明,再把“哪些变量是构建时必须的”写到文档里。这样团队在改站点配置时,不会只能靠口头约定。

案例 8:React 里常见的类型模式,这个仓库已经有不少

这一块其实是当前仓库里最值得直接补给读者的内容,因为都已经有现成代码。

8.1 children: ReactNode

src/components/BrowserWindow/index.tsxsrc/components/CommonLayout/index.tsx 都在用:

children: ReactNode;

这适合“容器类组件”,也就是组件本身不关心子节点的具体结构,只负责包一层布局、样式或者上下文。

8.2 React.ComponentProps<"svg">

src/components/HomepageFeatures/index.tsx 里有一个很好的例子:

Svg: React.ComponentType<React.ComponentProps<"svg">>;

这个写法很值得单独记一下,因为它表达的是:

  • 这里需要的是一个 React 组件
  • 这个组件要接收一组标准 svg 标签支持的 props

它比手写一份“差不多的 SVG props”稳得多,也更适合接第三方 icon 组件或静态资源组件。

8.3 直接复用框架 Props

前面提过主题覆写的 Props,像 src/theme/DocRoot/Layout/Main/index.tsx 也是同一类思路:

import type { Props } from "@theme/DocRoot/Layout/Main";

对于框架提供的页面壳、布局组件、路由组件,优先复用它现成的 Props,通常都比自己猜接口更稳。

8.4 原生事件和 React 事件要分清

src/pages/index.tsx 里有这段:

const onMove = (event: MouseEvent) => {
// ...
};

heroAside.addEventListener("mousemove", onMove);

这里用的是原生 DOM 事件,所以类型写 MouseEvent 是对的。

如果你是在 JSX 里写:

<button onClick={(event) => {}} />

那更常见的是 React.MouseEvent<HTMLButtonElement>

这两个别混:

  • addEventListener 场景:更偏原生 DOM 事件
  • JSX 事件回调场景:更偏 React SyntheticEvent 体系

8.5 表单和输入事件的常见写法

在普通 React 项目里,最常用的几种事件类型通常是:

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
};

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log(event.currentTarget);
};

这类内容虽然当前仓库源码里还不算多,但非常值得补到 TypeScript 章节里,因为几乎所有 React 项目都会频繁遇到。

案例 9:第三方库类型优先“复用或安装”,再考虑自写声明

从当前仓库的依赖结构也能看出一个倾向:能直接用上游类型的地方,就尽量不要自己重新造一份。

比如这里已经直接依赖了:

  • @docusaurus/types
  • @types/node
  • @types/react

这背后的实践原则其实很通用:

  1. 先看库本身有没有自带类型
  2. 没有再看社区有没有 @types/*
  3. 都没有,再考虑自己补 declare module

这样后续升级依赖时,类型系统更不容易和真实实现脱节。

现有 TypeScript 章节还值得继续补的点

除了这次已经补上的“接入”“项目案例”“环境变量声明”“第三方库类型补充”和“React 常见类型模式”,我觉得后面还可以继续补这几类内容:

  • 运行时校验和类型系统的关系
  • API 层通用类型设计:ApiResponse<T>、分页结果、错误对象
  • 类型收敛策略:什么时候该写联合类型,什么时候先保留 string
  • 老项目迁移策略:怎么逐步减少 any

建议怎么读这一组

如果你是第一次系统整理 TypeScript,可以按这个顺序:

  1. TypeScript 基础
  2. TypeScript 泛型
  3. TypeScript 高级特性
  4. TypeScript 项目接入
  5. TypeScript 项目案例与配置

前 3 篇补语言,后 2 篇补落地。

如果你想继续看“类型系统之外,运行时边界怎么守住”,更适合接着读 运行时校验专题

如果你特别想看一条和 TypeScript 表达更贴近的路线,也可以直接进 ArkType:最接近 TypeScript 的运行时校验

延伸阅读:如果项目是 Agent / AI 系统

如果你想看的不是普通 Web 项目,而是:

  • Agent
  • Tool Calling
  • 多轮状态流转
  • Memory / Evals / Runtime

那更推荐直接去 AI 模块继续看:

那篇会更集中地讲 tool、message、state、memory、run result 这些边界在 Agent 工程里怎么收口。