很多团队引入 TypeScript 的方式是「先把文件后缀改成 .ts,报错就加 any」,结果类型全是摆设,重构时照样心虚。真正值钱的不是「有类型」,而是让类型成为编译期的测试用例:它帮你证明边界情况被处理了、重构时不会悄悄破坏调用方。这篇文章按「语法 → 思维 → 工程」的顺序,把 TypeScript 从入门带到实战。
为什么值得用 TypeScript
- 编辑器即文档:跳转、补全、重命名全部可靠,新人接手不用读完全部源码;
- 重构底气:改一个函数的返回值,编译期就能列出所有没跟上的调用点;
- 边界建模:用联合类型把「不可能的状态」变成编译错误,而不是运行时的 if 补丁;
- 成本可控:严格度是渐进的,可以从最宽松的配置开始,一步步收紧。
基础类型与类型推断
TypeScript 的类型是「结构化的」——只描述形状,不关心你叫什么名字。大部分时候不需要手写类型,编译器能推断出来:
// 推断:不需要写类型
let count = 0; // number
const names = ['Alice', 'Bob']; // string[]
const user = { name: 'Fadey', age: 30 }; // { name: string; age: number }
// 需要显式标注的典型场景:函数参数、导出边界、复杂结构
function greet(name: string): string {
return `你好,${name}`;
}
// 数组 / 元组 / 字面量联合
type Status = 'todo' | 'doing' | 'done'; // 字面量联合,比 enum 轻
const pair: [string, number] = ['age', 30];
注意 any 和 unknown 的区别:any 会关掉检查并且传染;unknown 是「安全的任意值」,用之前必须收窄。优先用 unknown 接外部数据(接口返回、JSON.parse)。
interface 还是 type
这个问题争论了很多年,实际结论很务实:
- 对象形状用
interface:可继承、可合并声明、报错信息更友好; - 联合 / 交叉 / 映射用
type:type Status = 'a' | 'b'无法用 interface 表达; - 团队里二选一并写进规范,混用没有技术理由。
interface User {
id: number;
name: string;
}
// 交叉类型组合
type Admin = User & { role: 'admin'; permissions: string[] };
// 工具类型拼装新形状
type UserPreview = Pick<User, 'id' | 'name'>;
泛型:给函数加上「类型参数」
泛型的本质是类型层面上的函数。写泛型最常见的错误是「为了泛型而泛型」,判断标准很简单:调用方能不能因为泛型得到更精确的类型?
// 泛型 + keyof:返回值类型跟着 key 走
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Fadey', age: 30 };
const n = getProperty(user, 'age'); // n: number —— 精确到字段类型
// 约束 + 默认值
function paginate<T extends { id: number }>(items: T[], size = 10): T[][] {
const pages: T[][] = [];
for (let i = 0; i < items.length; i += size) pages.push(items.slice(i, i + size));
return pages;
}
联合类型与类型收窄
TypeScript 最有生产力的部分,是用收窄(narrowing)把联合类型一步步切开。它认识 typeof、in、instanceof、可辨识字段等标准写法:
type Result =
| { ok: true; data: string[] }
| { ok: false; error: string };
function render(r: Result) {
if (r.ok) {
// 这里 r 被收窄为 { ok: true; data: string[] }
return r.data.join(', ');
}
// 这里 r 只剩 { ok: false; error: string }
return `失败: ${r.error}`;
}
// 判别字段用字面量类型,是「不可能状态」建模的核心技巧
type FetchState =
| { state: 'loading' }
| { state: 'success'; items: string[] }
| { state: 'error'; message: string };
当联合分支太多时,switch + never 可以强制穷举:漏掉一个分支,编译器就报错——这正是「把运行时 bug 提前到编译期」的典型用法。
工具类型:少写重复的接口
interface Article {
id: number;
title: string;
body: string;
tags: string[];
}
type ArticleDraft = Partial<Article>; // 全部可选(表单草稿)
type ArticleCard = Pick<Article, 'id' | 'title'>; // 只取展示字段
type ArticleMeta = Omit<Article, 'body'>; // 去掉正文
type TagIndex = Record<string, string[]>; // 索引签名
type ArticleId = Article['id']; // 取字段类型
type GetArticle = ReturnType<typeof fetchArticle>; // 取函数返回类型
declare function fetchArticle(id: number): Promise<Article>;
这四个(Partial / Pick / Omit / Record)加上 ReturnType,能覆盖日常 80% 的模型派生需求。再往上是「类型体操」,只有在写库(SDK、ORM、状态管理)时才值得投入。
tsconfig:从宽松走向严格
新项目直接开全程严格;老项目按顺序逐步打开,每步配套修复。关键的几项:
{
"compilerOptions": {
"strict": true, // 打开全部严格检查(新项目必须)
"noUncheckedIndexedAccess": true, // arr[0] 的类型是 T | undefined
"exactOptionalPropertyTypes": true, // 可选属性不允许显式赋 undefined
"noImplicitOverride": true,
"verbatimModuleSyntax": true, // 类型导入必须写 import type
"skipLibCheck": true, // 跳过第三方声明文件检查,加快编译
"moduleResolution": "bundler",
"target": "ES2022"
}
}
迁移老项目推荐顺序:strictNullChecks → noImplicitAny → strict 全家桶 → 增量开启上面这几项。每打开一项就修一批,不要攒在一起。
从 JavaScript 渐进迁移的实操路径
- 先允许混合:
allowJs: true,让 .js 和 .ts 共存,迁移一个文件算一个; - 从叶子开始:工具函数、常量、纯逻辑模块先迁(依赖少、收益快);
- 给外部数据建类型:接口返回值、localStorage、环境变量,用
unknown接住再收窄; - 禁止新 any:ESLint 挂
@typescript-eslint/no-explicit-any,存量容忍、增量封死; - 用类型测试收尾:给关键公共函数写
expectTypeOf断言,防止后来者改坏类型契约。
常见坑与对策
- as 断言滥用:断言会让编译器闭嘴,但错误还在。规则:能用收窄就别用断言,
as unknown as X属于红色警报; - 枚举(enum):会生成运行时代码、和字面量联合语义重叠,新代码建议直接用字面量联合或
as const对象; - 类型文件循环引用:把共享类型放进独立的
types.ts,避免 a.ts ↔ b.ts 互导; - 编译太慢:优先
skipLibCheck、拆小 tsconfig(references)、必要时tsc --generateTrace找大类型; - 只检查不构建:Vite / esbuild 只做转译不做类型检查,CI 里必须单独跑一次
tsc --noEmit。
类型的价值不在于「写得更复杂」,而在于让错误在编译期暴露、让重构变得敢做。先让类型帮你看住边界,再谈类型体操。
总结
TypeScript 的学习曲线不在于语法,而在于思维方式的转换:用类型描述数据形状,用联合类型穷举状态,用收窄和 never 让遗漏无处可藏。本文覆盖的基础类型、泛型、收窄、工具类型和 tsconfig 严格化,构成了一条从零到工程化的完整路径。下一步建议找一个自己的小项目,把 strict 打开、把 any 清零,那种「改代码不慌」的感觉,就是类型系统最真实的回报。