下载工作台
前端框架精通

TypeScript 入门与类型系统

试读上半部分 · 解锁后可读全文

第 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 buildvue-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' | ...
场景推荐
对象、类、extendsinterface
联合、元组、工具类型组合type
需要声明合并(扩展现有全局)interface
React 组件 Props(团队习惯)两者均有,本模块倾向 type

贤紫约定:领域实体(CourseOrder)用 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开启严格族新项目必开
strictNullChecksnull/undefined 区分减少线上空指针
noImplicitAny禁止隐式 any配合渐进迁移可暂关老目录
moduleResolution: bundlerVite/esbuild 友好Vite 5+ 推荐
paths路径别名vite.config 同步
skipLibCheck跳过 d.ts 检查加速编译,CI 可保留
allowJs允许 JS 共存迁移期开启
checkJs对 JS 做类型检查可选,JSDoc 场景

Vue 项目常增加:

{
  "compilerOptions": {
    "jsx": "preserve"
  },
  "vueCompilerOptions": {
    "target": 3
  }
}

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。