TypeScript 入门别只背类型:从 tsconfig 开始建立工程意识
很多 TypeScript 教程都是从 string、number、interface 开始。学到泛型时,感觉自己都会了;回到真实项目,看到一份 tsconfig.json 和一堆类型报错,还是不知道该从哪下手。
TypeScript 给 JavaScript 增加类型检查。程序跑起来之前,它会先把对象形状不对、undefined 没处理、参数传错这些问题挑出来。
下面按真实开发的顺序讲。先让项目跑起来,再学那些会反复遇到的类型能力。
从项目依赖安装,不要把版本交给全局环境
正式项目里,把 TypeScript 装进项目开发依赖:
npm install -D typescriptnpx tsc -v这样版本会写进 package.json 和 lockfile,协作的人跑到的是同一套编译器。全局安装适合临时试验,但它很容易让 “我电脑能过、你电脑不行” 变成日常。
初始化配置:
npx tsc --initTypeScript 5.9 的默认初始化模板已经把 strict、noUncheckedIndexedAccess、exactOptionalPropertyTypes 等更严格的选项放进推荐项里。TypeScript 6.0 还改变了一个容易踩的行为:目录存在 tsconfig.json 时,再给 tsc 直接传源码文件会报错;如果只是临时检查单文件,需要显式加 --ignoreConfig。
这不是故意为难人。项目检查和单文件试验本来就是两件事,混在一起反而容易让你以为配置生效了,实际没有。
先写一份能看懂的 tsconfig
不同项目的模块设置不一样。Node.js、Vite、Next.js 都有各自的约定,所以不该复制一份 “万能配置”。但新项目可以从下面的核心项理解起:
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "verbatimModuleSyntax": true, "isolatedModules": true, "skipLibCheck": true }, "include": ["src"]}如果是 Vite 这类 bundler 项目,通常由模板选择 moduleResolution: "bundler"。不要为了让报错消失,把它随手改成另一个值;先看框架生成的配置和官方文档。
其中真正决定体验的是这几个:
strict: true:打开严格类型检查,是新项目的基线。noUncheckedIndexedAccess: true:数组或字典取值会带上undefined,逼着你处理越界和缺失键。exactOptionalPropertyTypes: true:区分 “字段不存在” 和 “字段存在但值是undefined”。verbatimModuleSyntax: true:让类型导入写清楚,模块行为更可预测。skipLibCheck: true:不检查依赖的声明文件,通常能减少无关的构建噪音。
TypeScript 官网对每一项都有说明。先理解 strict 和 noUncheckedIndexedAccess,其余选项可以随着项目遇到的问题再开启。
类型写在数据边界,而不是每个变量上
刚学 TS 常犯的错,是给每个变量写注解:
const name: string = "MarxChou";const count: number = 3;编译器本来就能推断出来,写了只是增加噪音。类型最值得写在边界:函数参数、函数返回值、API 数据、组件 props、环境变量,以及任何来自用户和网络的输入。
type User = { id: string; name: string; email?: string;};
function formatUser(user: User): string { return user.email ? `${user.name} <${user.email}>` : user.name;}email? 的意思不是 “总会有一个 undefined”。而是这个字段可能根本不存在。使用它前判断一次,后面的代码就干净了。
type 和 interface 在描述对象时都能用。新手不必花时间争论站队:对象模型、可扩展的公开接口常见 interface;联合类型、映射和组合写成 type 通常更顺手。团队已有风格就跟团队走。
unknown 比 any 多一道真正有用的门
any 最危险的地方不是它会报错,而是它不报错。拿到外部 JSON 后直接断言成业务类型,编辑器会相信你;数据缺字段时,程序才会在运行时掉坑里。
const payload: unknown = JSON.parse(text);
if ( typeof payload === "object" && payload !== null && "id" in payload && typeof payload.id === "string") { console.log(payload.id);}这叫类型收窄。你先拿到 unknown,再用 typeof、in、Array.isArray 或自定义类型守卫,把它缩小到可以安全使用的范围。
API 数据复杂时,手写判断会变长。可以用 Zod、Valibot 这类 schema 工具,但逻辑不变:网络输入先验证,再进入业务代码。别让 as User 变成项目里最常见的一句咒语。
联合类型比 “万能对象” 更贴近真实状态
很多状态不是 “一个对象,有些字段可能为空”,而是几个明确的分支。联合类型把这些分支写出来,代码自然会处理完整。
type RequestState<T> = | { status: "idle" } | { status: "loading" } | { status: "success"; data: T } | { status: "error"; message: string };
function renderUser(state: RequestState<User>): string { switch (state.status) { case "idle": return "尚未请求"; case "loading": return "加载中"; case "success": return state.data.name; case "error":3 collapsed lines
return state.message; }}在 success 分支,data 自动存在;在 error 分支,才可以访问 message。这比堆叠 data?、error? 和布尔值可靠得多。
泛型解决的是 “保留关系”,不是炫技
泛型第一次难,不是语法复杂,而是不容易看出它在保留什么。
看一个简单函数:
function first<T>(items: T[]): T | undefined { return items[0];}
const firstId = first(["a", "b"]); // string | undefinedconst firstCount = first([1, 2]); // number | undefinedT 的作用是告诉编译器:输入数组是什么元素,输出就保持同一种元素。它不需要提前知道是字符串还是数字,但不会把两者弄混。
写 API 包装、分页函数、表格组件、事件工具时,泛型会反复出现。先把 “输入和输出之间的类型关系不丢” 这件事想明白,比背 extends、条件类型和映射类型有用得多。
处理数组和字典时,接受 undefined
启用 noUncheckedIndexedAccess 后,下面这段会提示风险:
const labels: Record<string, string> = { zh: "中文"};
const label = labels["en"]; // string | undefined这不是 TS 太严格,en 本来就可能不存在。正确处理方式取决于业务:提供默认值、返回错误,或者在入口处保证 key 合法。
const label = labels["en"] ?? "Unknown";把不确定性显式写出来,后面维护代码的人才知道这里真的可能缺数据。
模块和类型导入要分清
现代项目常见这种写法:
import type { User } from "./types.js";import { getUser } from "./api.js";import type 只为类型检查存在,运行时不会产生对应导入。配合 verbatimModuleSyntax,编译器不再替你猜某个 import 到底要不要保留,ESM 行为更清楚。
Node.js 项目还要特别注意模块解析。选了 NodeNext,相对路径的扩展名、package.json 的 type 字段、CommonJS 和 ESM 的边界都会影响最终产物。遇到导入错误先看这三处,不要第一反应就是把类型检查关掉。
从 JavaScript 迁移,别试图一天改完
老项目上 TypeScript,推荐从不改运行逻辑的步骤开始:
- 允许
.js与.ts并存,把allowJs和checkJs作为过渡工具。 - 优先迁移数据模型稳定、依赖少的模块,例如工具函数和 API 类型。
- 外部输入仍然先当作
unknown,不要用any把报错盖住。 - 最后再逐步打开更严格的配置项,处理每一类报错的根因。
迁移的目标不是让 tsc 安静,而是让真正可能出错的边界被描述出来。一个到处 any 的 .ts 项目,维护成本不会比 JavaScript 低。
给新项目的一张检查单
[ ] TypeScript 作为项目 devDependency 安装[ ] tsconfig 启用 strict[ ] API 和用户输入先验证,不直接相信 as 断言[ ] 函数参数、返回值、组件 props 有清晰的类型边界[ ] 请求状态用联合类型表达,而不是多个可选字段[ ] 数组和字典访问处理 undefined[ ] 团队统一模块解析和 type-only import 的写法TypeScript 的价值不在于写出最长的类型,而在于让错误尽量靠近它发生的地方。写到这里,类型系统才开始替你省时间。
