第 8 章 · TypeScript 入门与类型系统
本章目标:理解企业项目为何采用 TypeScript;掌握基础类型、interface vs type、泛型、联合类型;配置 tsconfig.json 关键选项;制定与 JS 的渐进迁移策略;完成可复用的 utils.ts 类型示例,为 ch09 Vue、ch10 React 打底。
学时建议:4~5 小时(含 90 分钟跟练)
前置:ES6+、本模块 ch01~ch07;了解 JSON 与 REST 接口基本概念。
8.1 为何企业项目用 TypeScript
| 痛点(纯 JS) | TS 如何缓解 | 贤紫商城实例 |
|---|---|---|
| 接口字段改名后端未通知 | 编译期报错 | order.status 枚举与 OpenAPI 同步 |
undefined 运行时崩溃 | 可选链 + 严格空值检查 | 商品 cover 缺省时的列表图 |
| 重构不敢动 | 类型引导重命名 | Pinia store 字段统一迁移 |
| 新人误用 API | 函数签名即文档 | createOrder(payload) 必填字段 |
| 多仓库协作 | 共享 types 包 | 学院 Mock 与后台表单同型 |
开发阶段 生产阶段
TS 源码 ──编译──► JS ──► 浏览器
│ (类型已擦除,零运行时成本)
└── tsc / vue-tsc 报错拦截
行业案例 · 贤紫优选商城:2025 年商户后台新模块强制.ts/<script setup lang="ts">;老.js页通过allowJs共存,每季度下线 20% JS 文件。接口类型由 OpenAPI 生成api/types.d.ts,联调返工率下降约 35%。
8.2 环境搭建
# 在 academy 工作区新建类型练习包(可选)
mkdir academy-ts-lab && cd academy-ts-lab
npm init -y
npm install typescript --save-dev
npx tsc --init
package.json 脚本:
{
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc"
}
}
与 Vite 项目:依赖已带 TS,执行 npm run build 或 vue-tsc --noEmit(ch09)。
8.3 基础类型
// 标量与字面量
const title: string = '贤紫技术学院'
const hours: number = 12
const isFree: boolean = true
const level: 'beginner' | 'intermediate' | 'advanced' = 'beginner'
// 数组与元组
const tags: string[] = ['Vue', 'React']
const pair: [string, number] = ['fe-01', 8]
// 对象
const course: { id: string; title: string; hours?: number } = {
id: 'fe-01',
title: 'HTML 基础'
}
// 任意与未知(慎用 any,优先 unknown)
let payload: unknown = await res.json()
if (typeof payload === 'object' && payload !== null && 'id' in payload) {
// 收窄后使用
}
// void / never
function log(msg: string): void {
console.log(msg)
}
function assertNever(x: never): never {
throw new Error('unexpected: ' + x)
}
| 类型 | 用途 |
|---|---|
string / number / boolean | 标量 |
| 字面量联合 | 枚举替代、状态机 |
readonly | 不可变配置 |
? 可选属性 | 接口缺省字段 |
unknown | 安全接收外部数据 |
8.4 interface vs type
两者大多可互换;掌握差异场景即可。
// interface — 可声明合并,适合对象形状、类实现
interface Course {
id: string
title: string
}
interface Course {
hours?: number // 合并进 Course
}
// type — 可表达联合、交叉、映射
type CourseId = string
type Level = 'beginner' | 'intermediate' | 'advanced'
type Course = {
id: CourseId
title: string
level: Level
tags: string[]
}
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string }
type ReadonlyCourse = Readonly<Course>
type CourseKeys = keyof Course // 'id' | 'title' | ...
| 场景 | 推荐 |
|---|---|
对象、类、extends | interface |
| 联合、元组、工具类型组合 | type |
| 需要声明合并(扩展现有全局) | interface |
| React 组件 Props(团队习惯) | 两者均有,本模块倾向 type |
贤紫约定:领域实体(Course、Order)用 interface;API 响应联合、工具类型用 type。
8.5 联合类型与类型收窄
type PayStatus = 'pending' | 'paid' | 'refunded'
interface Order {
id: string
amount: number
status: PayStatus
}
function statusLabel(order: Order): string {
switch (order.status) {
case 'pending':
return '待支付'
case 'paid':
return '已支付'
case 'refunded':
return '已退款'
default:
return assertNever(order.status)
}
}
// 判别联合(Discriminated Union)
type LoadState<T> =
| { kind: 'loading' }
| { kind: 'error'; message: string }
| { kind: 'success'; data: T }
function renderCourses(state: LoadState<Course[]>) {
switch (state.kind) {
case 'loading':
return '加载中…'
case 'error':
return state.message
case 'success':
return state.data.map(c => c.title).join(', ')
}
}
academy-demo 三态 UI(loading / error / success)用判别联合可在编译期杜绝「有 data 却无 loading」的非法状态。
8.6 泛型
// 函数泛型
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
const a = first([1, 2, 3]) // number | undefined
const b = first(['fe-01', 'fe-02']) // string | undefined
// 泛型接口
interface Page<T> {
items: T[]
total: number
page: number
pageSize: number
}
type CoursePage = Page<Course>
// 泛型约束
interface HasId {
id: string
}
function findById<T extends HasId>(list: T[], id: string): T | undefined {
return list.find(item => item.id === id)
}
// 常用工具类型(内置)
type PartialCourse = Partial<Course>
type PickCourse = Pick<Course, 'id' | 'title'>
type OmitCourse = Omit<Course, 'tags'>
type RecordTags = Record<string, number>
Pinia / useState 中列表、分页响应大量使用 Page<T>、ApiResult<T>,避免重复定义。
8.7 tsconfig.json 关键选项
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client"]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
| 选项 | 说明 | 建议 |
|---|---|---|
strict | 开启严格族 | 新项目必开 |
strictNullChecks | null/undefined 区分 | 减少线上空指针 |
noImplicitAny | 禁止隐式 any | 配合渐进迁移可暂关老目录 |
moduleResolution: bundler | Vite/esbuild 友好 | Vite 5+ 推荐 |
paths | 路径别名 | 与 vite.config 同步 |
skipLibCheck | 跳过 d.ts 检查 | 加速编译,CI 可保留 |
allowJs | 允许 JS 共存 | 迁移期开启 |
checkJs | 对 JS 做类型检查 | 可选,JSDoc 场景 |
Vue 项目常增加:
{
"compilerOptions": {
"jsx": "preserve"
},
"vueCompilerOptions": {
"target": 3
}
}