Chapter 02

TypeScript 类型系统

静态类型让错误在编译期暴露,让代码在运行前就能"自描述"

1. 什么是 TypeScript

TypeScript(TS)是由微软开发并开源的 JavaScript 超集。"超集"意味着所有合法的 JS 代码都是合法的 TS 代码,你可以渐进式地迁移现有项目。

核心特征

为什么前端项目要用 TS

// 没有 TypeScript:运行时才发现错误
function greet(user) {
  return `Hello, ${user.naem}!`; // 拼写错误 naem,运行时才报 undefined
}

// 有 TypeScript:编写时就发现错误
interface User { name: string; age: number; }
function greet(user: User) {
  return `Hello, ${user.naem}!`; // TS 错误: Property 'naem' does not exist
}

2. 基础类型

TypeScript 的基础类型与 JavaScript 的运行时类型对应,但增加了几个重要的编译期专有类型。

原始类型

let name: string = 'Alice';
let age: number = 25;         // 整数和浮点数都是 number
let active: boolean = true;
let nothing: null = null;
let undef: undefined = undefined;
let id: symbol = Symbol('uid');
let bigNum: bigint = 9007199254740991n;

unknown vs any(重要!)

any 关闭了类型检查——你可以对 any 类型的值做任何操作,TS 不会报错。unknown 是"类型安全的 any"——你必须先做类型检查(缩窄),才能操作它。

// any —— 逃生舱,但很危险
let data: any = fetchSomething();
data.foo().bar.baz(); // TS 完全不检查,运行时可能崩溃

// unknown —— 安全的"不知道是什么类型"
let value: unknown = fetchSomething();
value.toUpperCase(); // ❌ TS 错误:必须先检查类型

// 必须先缩窄类型才能使用
if (typeof value === 'string') {
  value.toUpperCase(); // ✅ 现在知道是 string 了
}
⚠️

原则:尽量不用 any。接收外部数据(API 响应、用户输入)时用 unknown,然后用类型守卫缩窄。在代码库中大量使用 any 会让 TypeScript 的价值大打折扣。

never 类型

never 表示"永远不会发生的值"。函数抛出异常或无限循环时,返回值类型是 never。在穷举检查时也很有用。

// 永远不会正常返回的函数
function fail(msg: string): never {
  throw new Error(msg);
}

// 穷举检查 —— 如果有遗漏的 case,TS 会报错
type Shape = 'circle' | 'square' | 'triangle';
function getArea(shape: Shape): number {
  switch (shape) {
    case 'circle':   return Math.PI;
    case 'square':   return 1;
    case 'triangle': return 0.5;
    default:
      const exhausted: never = shape; // 如果漏掉某个 case,此处报错
      throw new Error(`Unknown: ${exhausted}`);
  }
}

3. 类型注解与类型推断

TypeScript 具备强大的类型推断能力——很多时候不需要手动标注,编译器能自动推断出类型。了解什么时候需要显式注解,什么时候可以省略,是写出"地道 TS"的关键。

// 可以推断 —— 无需注解
const name = 'Alice';           // 推断为 string
const age = 25;                 // 推断为 number (字面量类型 25)
const arr = [1, 2, 3];          // 推断为 number[]
const obj = { x: 1, y: 2 };    // 推断为 { x: number; y: number }

// 需要显式注解的情况

// 1. 函数参数(无法推断)
function add(a: number, b: number): number { return a + b; }

// 2. 初始值为 null/undefined(后续赋值时需要知道类型)
let user: User | null = null;

// 3. 需要明确比推断结果更宽的类型
let id: string | number = 1; // 推断会是 1(字面量),但我们想允许 string

// 4. 复杂对象(提高可读性)
const config: AppConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
};

4. 接口(Interface)

接口用于定义对象的"形状"——它有哪些属性,每个属性是什么类型。这是 TypeScript 中描述数据结构最常用的方式。

基本接口与可选/只读属性

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;          // ? 表示可选属性(可以不存在)
  readonly createdAt: Date; // readonly 表示只读,赋值后不可修改
}

const user: User = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  createdAt: new Date(),
};

user.name = 'Bob';       // ✅
user.createdAt = new Date(); // ❌ Cannot assign to 'createdAt' (readonly)

索引签名

当你不知道对象有哪些属性键,但知道值的类型时,使用索引签名。

interface StringMap {
  [key: string]: string; // 任意 string 键,值必须是 string
}

interface Scores {
  [studentId: string]: number;
  total: number;    // 可以和索引签名共存,但类型必须兼容
}

接口继承

interface Animal {
  name: string;
  eat(): void;
}

interface Pet extends Animal {
  owner: string;
  play(): void;
}

// 可以继承多个接口
interface ServiceDog extends Animal, Pet {
  certification: string;
}

接口 vs 类型别名(Interface vs Type Alias)

特性InterfaceType Alias
定义对象形状✅ 主要用途✅ 可以
继承 / 扩展✅ extends✅ 交叉类型 &
声明合并✅ 同名 interface 自动合并❌ 不支持
联合类型❌ 不支持✅ A | B
映射类型 / 条件类型
推荐场景对象、类、API 响应联合、交叉、复杂类型

5. 类型别名(Type Alias)

使用 type 关键字创建类型的"别名",可以给任何类型起一个有意义的名字。

字面量类型与联合类型

// 字面量类型 —— 只能是这几个值
type Direction = 'north' | 'south' | 'east' | 'west';
type Status    = 'pending' | 'fulfilled' | 'rejected';
type HttpCode  = 200 | 201 | 400 | 401 | 403 | 404 | 500;

// 联合类型 —— 可以是其中任意一种类型
type ID = string | number;
type Nullable<T> = T | null;

function move(direction: Direction) { /* ... */ }
move('north'); // ✅
move('up');    // ❌ Argument of type '"up"' is not assignable

交叉类型

type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDeletable = { deletedAt: Date | null };

// 交叉类型 —— 同时满足所有类型的属性
type Post = {
  title: string;
  content: string;
} & Timestamped & SoftDeletable;

// Post 必须同时有 title, content, createdAt, updatedAt, deletedAt

6. 泛型(Generics)

泛型让你编写可以处理多种类型的代码,同时保持类型安全。把泛型参数理解为"类型的占位符"——使用时再传入具体类型。

泛型函数

// 没有泛型 —— 只能处理 string
function firstString(arr: string[]): string | undefined {
  return arr[0];
}

// 有泛型 T —— 输入什么类型的数组,返回同类型
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const s = first(['a', 'b']); // 推断 T = string,返回 string | undefined
const n = first([1, 2, 3]);   // 推断 T = number,返回 number | undefined

// 多个类型参数
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

泛型接口与约束

// 泛型接口
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

type UserResponse = ApiResponse<User>;
type PostListResponse = ApiResponse<Post[]>;

// 泛型约束(T 必须有 id 属性)
function findById<T extends { id: number }>(
  items: T[],
  id: number
): T | undefined {
  return items.find(item => item.id === id);
}

// 约束 K 必须是 T 的键(keyof)
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

内置泛型工具类型

工具类型作用示例
Partial<T>所有属性变可选Partial<User>
Required<T>所有属性变必填Required<Options>
Pick<T, K>只保留指定属性Pick<User, 'id' | 'name'>
Omit<T, K>排除指定属性Omit<User, 'password'>
Record<K, V>键为 K、值为 V 的对象Record<string, number>
ReturnType<F>获取函数返回值类型ReturnType<typeof fetchUser>
// Partial 常用于更新操作(只需要传部分字段)
function updateUser(id: number, data: Partial<User>) {
  // data 中的所有属性都是可选的
}
updateUser(1, { name: 'Bob' }); // 只更新 name,合法

// Omit 用于创建"不含密码的用户"类型
type PublicUser = Omit<User, 'password' | 'secretKey'>;

7. 类型守卫(Type Guards)

类型守卫是在运行时缩窄(narrow)类型的手段——通过条件判断让 TypeScript 知道在某个代码分支中,变量是什么具体类型。

type StringOrNumber = string | number;

function process(value: StringOrNumber) {
  // typeof 类型守卫
  if (typeof value === 'string') {
    // 此分支中 value 是 string
    console.log(value.toUpperCase());
  } else {
    // 此分支中 value 是 number
    console.log(value.toFixed(2));
  }
}

// instanceof 类型守卫
function handleError(err: unknown) {
  if (err instanceof Error) {
    console.log(err.message); // err 是 Error 类型
  }
}

// in 类型守卫(检查属性是否存在)
interface Cat { meow(): void }
interface Dog { bark(): void }

function makeSound(animal: Cat | Dog) {
  if ('meow' in animal) {
    animal.meow(); // animal 是 Cat
  } else {
    animal.bark(); // animal 是 Dog
  }
}

// 自定义类型谓词(is)
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

const items: unknown[] = ['hello', 42, true, 'world'];
const strings = items.filter(isString); // 推断为 string[]

8. 枚举(Enum)

枚举用于定义一组命名的常量,让代码更易读。TypeScript 支持数字枚举和字符串枚举。

// 数字枚举(默认从 0 开始递增)
enum Direction {
  Up,       // 0
  Down,     // 1
  Left,     // 2
  Right,    // 3
}

// 字符串枚举(推荐 —— 调试更友好)
enum Status {
  Pending   = 'PENDING',
  Active    = 'ACTIVE',
  Inactive  = 'INACTIVE',
}

function handleStatus(s: Status) {
  if (s === Status.Active) {
    console.log('Active!');
  }
}

// const enum —— 编译后直接内联值,不生成 JS 对象(性能优化)
const enum HttpMethod {
  GET = 'GET',
  POST = 'POST',
  DELETE = 'DELETE',
}
ℹ️

现代趋势:很多项目用字面量联合类型代替枚举(如 type Status = 'pending' | 'active' | 'inactive'),因为更简洁、与 JSON 数据自然对应。两种方式都可以,了解即可。

9. React 中的 TypeScript 实战

在实际的 React 项目中,TypeScript 最常用于:Props 类型定义、Hook 泛型、事件类型。

组件 Props 类型

// 定义 Props 接口
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  children?: React.ReactNode; // 接受任意 React 内容
}

// 函数组件 —— 推荐省略 React.FC,直接用 props 类型
function Button({
  label,
  onClick,
  variant = 'primary',
  disabled = false
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

事件类型

function Form() {
  // input onChange 事件
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  // button onClick 事件
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
  };

  // form onSubmit 事件
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button onClick={handleClick}>Submit</button>
    </form>
  );
}

useState 与 useRef 泛型

import { useState, useRef } from 'react';

function Counter() {
  // useState 泛型 —— 通常可以推断,复杂类型需要显式指定
  const [count, setCount] = useState<number>(0);
  const [user, setUser] = useState<User | null>(null);

  // useRef 泛型 —— 指定 DOM 元素类型
  const inputRef = useRef<HTMLInputElement>(null);
  const focusInput = () => {
    inputRef.current?.focus(); // current 可能是 null,用 ?. 安全调用
  };

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={focusInput}>Focus</button>
    </div>
  );
}

本章小结:TypeScript 的核心价值在于类型安全和开发体验。重点掌握:Interface/Type 定义数据结构、泛型写可复用代码、类型守卫缩窄联合类型、React 中的 Props 和事件类型。