TypeScript 现代编程教程

面向有 JavaScript 基础的开发者,系统学习 TypeScript 类型系统与工程实践。所有示例均为 TypeScript 5.x,可直接运行。


目录

  1. 为什么需要 TypeScript
  2. 环境搭建与编译配置
  3. 基础类型注解
  4. 接口与类型别名
  5. 联合类型与交叉类型
  6. 泛型
  7. 条件类型与映射类型
  8. 函数类型进阶
  9. 类与访问修饰符
  10. 枚举与字面量
  11. 类型守卫与类型收窄
  12. 工具类型(Utility Types)
  13. 模块与声明文件
  14. 异步类型(Promise / async)
  15. 实战:类型安全的 Todo 应用

1. 为什么需要 TypeScript

1.1 JavaScript 的痛点

// 这段 JS 代码在运行时才会暴露问题
function add(a, b) {
  return a + b;
}

add(1, 2);      // 3
add("1", "2");  // "12" — 静默地做了字符串拼接
add(1, null);   // 1 — null 被转为 0,没有报错

TypeScript 在编译阶段就能捕获这些问题:

function add(a: number, b: number): number {
  return a + b;
}

add(1, 2);       // ✅
// add("1", "2"); // ❌ 编译报错:类型 "string" 不能赋值给 "number"

1.2 核心价值

能力 说明
静态类型检查 编码时发现类型错误,而非运行时
智能提示 IDE 自动补全、参数提示、跳转定义
重构安全 改名、改签名时自动定位所有引用
自文档化 类型定义本身就是最好的文档

2. 环境搭建与编译配置

2.1 安装与初始化

npm install -g typescript
tsc --version          # 应输出 5.x

# 在项目中初始化
mkdir my-project && cd my-project
tsc --init             # 生成 tsconfig.json

2.2 tsconfig.json 核心配置

{
  "compilerOptions": {
    "target": "ES2022",              // 编译目标 JS 版本
    "module": "ESNext",              // 模块系统
    "moduleResolution": "bundler",   // 模块解析策略(现代项目用 bundler)
    "strict": true,                  // ⭐ 开启所有严格检查
    "noUncheckedIndexedAccess": true,// 索引访问可能为 undefined
    "esModuleInterop": true,         // 兼容 CommonJS 默认导入
    "skipLibCheck": true,            // 跳过 .d.ts 类型检查(加速编译)
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",              // 编译输出目录
    "rootDir": "./src"               // 源码目录
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

2.3 运行方式

# 只做类型检查,不产出文件
tsc --noEmit

# 编译并输出 JS
tsc

# watch 模式
tsc --watch

# 开发时直接用 tsx 运行(推荐)
npx tsx src/index.ts

3. 基础类型注解

3.1 原始类型

let isDone: boolean = false;
let count: number = 42;
let name: string = "张三";
let nothing: null = null;
let notDefined: undefined = undefined;
let big: bigint = 100n;        // 大整数
let unique: symbol = Symbol("id");

3.2 数组与元组

// 数组:两种写法等价
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];  // 泛型写法

// 只读数组
const readonlyNums: readonly number[] = [1, 2, 3];
// readonlyNums.push(4);  // ❌ 报错

// 元组:固定长度和类型组合
let pair: [string, number] = ["age", 25];
// pair = [25, "age"];    // ❌ 类型顺序不对

// 具名元组(可读性更好)
type ApiResult = [success: boolean, data: string, code: number];
const result: ApiResult = [true, '{"id":1}', 200];

3.3 对象类型

// 直接注解
const user: { name: string; age: number } = {
  name: "张三",
  age: 25,
};

// 可选属性用 ?
const config: { host: string; port?: number } = {
  host: "localhost",
  // port 可以省略
};

// readonly 属性
const point: { readonly x: number; readonly y: number } = { x: 10, y: 20 };
// point.x = 30;  // ❌ 只读

// 索引签名:允许任意键
const dict: { [key: string]: number } = {
  apples: 3,
  bananas: 5,
};

3.4 any、unknown、never

// any:放弃类型检查(尽量少用)
let loose: any = 4;
loose = "hello";     // 不报错
loose.foo.bar();     // 也不报错(运行时可能炸)

// unknown:类型安全的 "未知"
let input: unknown = "hello";
// input.toUpperCase();  // ❌ 必须先收窄类型
if (typeof input === "string") {
  input.toUpperCase();   // ✅ 收窄后可用
}

// never:永远不会出现的类型
function throwError(msg: string): never {
  throw new Error(msg);
}

function infiniteLoop(): never {
  while (true) {}
}

3.5 类型断言

// as 语法(推荐)
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;

// 尖括号语法(JSX 中不能用)
const canvas2 = <HTMLCanvasElement>document.getElementById("myCanvas");

// 双重断言:先断言为 unknown,再断言为目标类型(危险,谨慎使用)
const value: string = (42 as unknown) as string;

4. 接口与类型别名

4.1 接口(interface)

interface User {
  readonly id: number;
  name: string;
  email: string;
  age?: number;              // 可选
  tags: string[];
}

// 使用
const u1: User = {
  id: 1,
  name: "张三",
  email: "zhang@example.com",
  tags: ["admin"],
};

// 扩展接口
interface Admin extends User {
  permissions: string[];
  level: number;
}

const admin: Admin = {
  id: 2,
  name: "管理员",
  email: "admin@example.com",
  tags: ["admin", "super"],
  permissions: ["delete", "manage"],
  level: 9,
};

4.2 类型别名(type)

// 基本类型别名
type ID = number | string;
type Point = { x: number; y: number };

// 联合类型
type Status = "idle" | "loading" | "success" | "error";

// 函数类型
type Callback = (data: string) => void;

// 交叉类型
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged & { city: string };

const p: Person = { name: "李四", age: 30, city: "北京" };

4.3 interface vs type 区别

// 1. interface 可以声明合并,type 不能
interface Window {
  title: string;
}
interface Window {
  ts: number;            // 自动合并
}
const win: Window = { title: "test", ts: 42 };

// 2. interface 只能描述对象,type 可以描述任意类型
type Name = string;                       // ✅ type 可以
// interface Name extends string {}       // ❌ interface 不行

// 3. type 可以做映射类型
type Nullable<T> = { [K in keyof T]: T[K] | null };  // ✅
// interface 做不到

// 选择策略:描述对象形状优先用 interface,其他场景用 type

5. 联合类型与交叉类型

5.1 联合类型(Union Types)

// 基本联合
type Result = "success" | "error";
type StatusCode = 200 | 201 | 400 | 404 | 500;

// 函数参数接受多种类型
function printId(id: number | string): void {
  console.log(`ID: ${id}`);
}

printId(101);       // ✅
printId("abc-123"); // ✅
// printId(true);   // ❌

// 联合类型需要收窄后才能使用特定方法
function getLength(value: string | string[]): number {
  return value.length;  // ✅ length 是共同的属性
}

function toUpperCase(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase();  // ✅ 收窄为 string
  }
  return value.toString();       // ✅ 收窄为 number
}

5.2 可辨识联合(Discriminated Unions)

这是 TypeScript 最强大的模式之一:

// 用公共字段来区分类型
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;     // shape 收窄为 circle
    case "rectangle":
      return shape.width * shape.height;       // shape 收窄为 rectangle
    case "triangle":
      return (shape.base * shape.height) / 2;  // shape 收窄为 triangle
  }
}

// 实际应用:API 响应
type ApiResponse =
  | { status: "ok"; data: User[] }
  | { status: "error"; code: number; message: string };

function handleResponse(resp: ApiResponse): void {
  if (resp.status === "ok") {
    console.log(`获取到 ${resp.data.length} 条数据`);
  } else {
    console.error(`错误 [${resp.code}]: ${resp.message}`);
  }
}

5.3 交叉类型(Intersection Types)

interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}

// 交叉 = 合并所有属性
type ColorfulCircle = Colorful & Circle;

const cc: ColorfulCircle = {
  color: "red",
  radius: 5,
};

// 函数混合(mixin 模式)
type Point2D = { x: number; y: number };
type WithId = { id: string };
type WithTimestamp = { createdAt: Date; updatedAt: Date };

type GeoPoint = Point2D & WithId & WithTimestamp;

6. 泛型

泛型是 TypeScript 类型系统最核心的抽象能力。

6.1 基本泛型

// 不使用泛型:丢失了类型信息
function first(arr: any[]): any {
  return arr[0];
}
const a = first([1, 2, 3]);  // a 的类型是 any

// 使用泛型:保留类型
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const a1 = first([1, 2, 3]);       // a1: number | undefined
const a2 = first(["a", "b"]);      // a2: string | undefined
const a3 = first([]);              // a3: undefined

6.2 泛型约束

// extends 约束泛型必须有某些属性
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): T {
  console.log(`长度: ${item.length}`);
  return item;
}

logLength("hello");        // 5
logLength([1, 2, 3]);     // 3
logLength({ length: 10 });// 10
// logLength(123);         // ❌ number 没有 length 属性

// 用 keyof 约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "张三", age: 25 };
getProperty(user, "name");    // ✅ 返回 string
getProperty(user, "age");     // ✅ 返回 number
// getProperty(user, "email"); // ❌ email 不是 user 的 key

6.3 泛型工具函数实战

// 交换元组元素
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

const swapped = swap([1, "hello"]);  // [string, number]

// 安全地合并对象
function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return { ...a, ...b };
}

const merged = merge({ name: "张三" }, { age: 25 });
// merged: { name: string } & { age: number }

// 状态机
function createState<T>(initial: T) {
  let state: T = initial;

  return {
    get: (): T => state,
    set: (next: T): void => { state = next; },
    update: (fn: (prev: T) => T): void => { state = fn(state); },
  };
}

const counter = createState(0);
counter.set(10);
counter.update((n) => n + 1);
console.log(counter.get());  // 11

6.4 泛型类

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop());  // 2

const stringStack = new Stack<string>();
stringStack.push("hello");

6.5 泛型默认值

// 不传泛型参数时使用默认类型
interface ApiConfig<T = Record<string, unknown>> {
  url: string;
  data?: T;
  timeout?: number;
}

// 简单请求
const simple: ApiConfig = { url: "/api/health" };

// 带类型的请求
interface LoginData {
  username: string;
  password: string;
}
const login: ApiConfig<LoginData> = {
  url: "/api/login",
  data: { username: "admin", password: "123456" },
};

7. 条件类型与映射类型

7.1 条件类型

// 语法:T extends U ? X : Y   在类型层面做判断
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;  // true
type B = IsString<number>;   // false

// 实用的条件类型
type NonNullable<T> = T extends null | undefined ? never : T;
type Flatten<T> = T extends (infer U)[] ? U : T;

type S = Flatten<string[]>;     // string
type N = Flatten<number>;       // number

7.2 infer 关键字

// 提取数组元素类型
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type El = ArrayElement<number[]>;  // number

// 提取函数返回类型(TS 内置的 ReturnType 就这样实现)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { name: "张三", age: 25 };
}
type UserType = MyReturnType<typeof getUser>;
// { name: string; age: number }

// 提取 Promise 内部类型
type Awaited<T> = T extends Promise<infer U> ? U : T;
type P = Awaited<Promise<string>>;  // string

7.3 映射类型(Mapped Types)

// 把对象所有属性变为可选
type Partial<T> = {
  [K in keyof T]?: T[K];
};

// 把对象所有属性变为只读
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

// 原始类型
interface User {
  name: string;
  age: number;
  email: string;
}

// 应用
type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string }

type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }

// 自定义:把所有属性变为 string 类型
type Stringify<T> = {
  [K in keyof T]: string;
};
type StringUser = Stringify<User>;
// { name: string; age: string; email: string }

7.4 模板字面量类型(Template Literal Types)

// Python / Rust 风格的字符串模式匹配
type EventName = `on${Capitalize<string>}`;
// "onClick" | "onChange" | "onSubmit" | ...

// 实际用例:CSS 类名
type Size = "sm" | "md" | "lg";
type Variant = "primary" | "secondary" | "danger";
type ClassName = `btn-${Variant}-${Size}`;
// "btn-primary-sm" | "btn-primary-md" | ... | "btn-danger-lg"

// 路由参数
type Route =
  | "/users"
  | `/users/${string}`
  | `/users/${string}/posts`;

function navigate(path: Route): void {}
navigate("/users");                     // ✅
navigate("/users/42");                  // ✅
navigate("/users/42/posts");           // ✅
// navigate("/admins");                // ❌

7.5 映射类型修饰符(+ / -)

// 去掉 readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

// 去掉可选
type Required<T> = {
  [K in keyof T]-?: T[K];
};

interface Config {
  readonly host: string;
  readonly port?: number;
}

type MutableConfig = Mutable<Config>;
// { host: string; port?: number | undefined }

type RequiredConfig = Required<Config>;
// { readonly host: string; readonly port: number }

8. 函数类型进阶

8.1 函数签名

// 完整的函数类型签名
type AddFn = (a: number, b: number) => number;

// 重载签名(多个签名 + 一个实现)
function format(value: number): string;
function format(value: string): string;
function format(value: boolean): string;
function format(value: number | string | boolean): string {
  return `格式化: ${value}`;
}

format(42);       // ✅
format("hello");  // ✅
format(true);     // ✅

// 泛型重载
function firstElement<T>(arr: T[]): T | undefined;
function firstElement<T>(arr: T, n: number): T;
function firstElement<T>(arr: T[], n?: number): T | T[] | undefined {
  if (n !== undefined) return arr[n];
  return arr[0];
}

8.2 this 参数

// 声明 this 的类型(第一个参数必须是 this,编译后会被移除)
interface Card {
  suit: string;
  card: number;
}

interface Deck {
  suits: string[];
  cards: number[];
  createCard(this: Deck, suitIndex: number, cardIndex: number): Card;
}

const deck: Deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  cards: Array.from({ length: 52 }, (_, i) => i + 1),
  createCard(this: Deck, suitIndex: number, cardIndex: number): Card {
    return {
      suit: this.suits[suitIndex]!,
      card: this.cards[cardIndex]!,
    };
  },
};

8.3 剩余参数与参数解构

// 剩余参数
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}

// 解构参数
function createUser({ name, age }: { name: string; age: number; email?: string }): string {
  return `${name}, ${age}`;
}

// 解构 + 剩余
interface SearchParams {
  keyword: string;
  page: number;
  sortBy?: string;
}

function search({ keyword, page, ...rest }: SearchParams): void {
  console.log(`搜索: ${keyword}, 第${page}页, 其他:`, rest);
}

8.4 不变量断言(as const)

// as const 让 TypeScript 推导出最窄的字面量类型
const colors = ["red", "green", "blue"] as const;
// 类型: readonly ["red", "green", "blue"]

type Color = (typeof colors)[number];  // "red" | "green" | "blue"

const config = {
  host: "localhost",
  port: 8080,
  retry: 3,
} as const;
// 类型: { readonly host: "localhost"; readonly port: 8080; readonly retry: 3 }

// 推导出精确值而非宽泛类型
const roles = {
  admin: "ADMIN",
  user: "USER",
  guest: "GUEST",
} as const;

type Role = (typeof roles)[keyof typeof roles];  // "ADMIN" | "USER" | "GUEST"

9. 类与访问修饰符

9.1 访问修饰符

class Animal {
  public name: string;           // 公开(默认)
  private _id: string;           // 私有:仅类内部可访问
  protected type: string;        // 受保护:类内部 + 子类可访问

  readonly species: string;      // 只读

  constructor(name: string, species: string) {
    this.name = name;
    this.species = species;
    this._id = crypto.randomUUID();
    this.type = "animal";
  }

  // getter / setter
  get id(): string {
    return this._id.slice(0, 8) + "...";
  }

  set id(value: string) {
    // 通常不提供 setter 或加验证
    throw new Error("不能手动设置 ID");
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name, "dog");
    console.log(this.type);    // ✅ 子类可以访问 protected
    // console.log(this._id);  // ❌ private 不行
  }
}

9.2 参数属性(简写构造器)

// 普通写法
class User {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

// 参数属性:一行搞定
class UserShort {
  constructor(
    public name: string,
    public age: number,
    private email: string,
    protected role: string = "user",
    readonly id: string = crypto.randomUUID(),
  ) {}
}

const u = new UserShort("张三", 25, "zhang@example.com");
console.log(u.name);   // ✅ public
// console.log(u.email); // ❌ private

9.3 抽象类与接口实现

interface Repository<T> {
  getAll(): Promise<T[]>;
  getById(id: string): Promise<T | null>;
  create(item: Omit<T, "id">): Promise<T>;
}

// 抽象类:部分实现,留给子类实现
abstract class BaseRepository<T extends { id: string }> implements Repository<T> {
  protected items: Map<string, T> = new Map();

  async getAll(): Promise<T[]> {
    return [...this.items.values()];
  }

  async getById(id: string): Promise<T | null> {
    return this.items.get(id) ?? null;
  }

  // 子类必须实现
  abstract create(item: Omit<T, "id">): Promise<T>;
}

// 具体实现
interface Task {
  id: string;
  title: string;
  done: boolean;
}

class TaskRepository extends BaseRepository<Task> {
  async create(item: Omit<Task, "id">): Promise<Task> {
    const task: Task = { ...item, id: crypto.randomUUID() };
    this.items.set(task.id, task);
    return task;
  }
}

9.4 静态成员

class MathUtils {
  static readonly PI = Math.PI;

  static clamp(value: number, min: number, max: number): number {
    return Math.min(Math.max(value, min), max);
  }

  static #instanceCount = 0;  // 静态私有

  static create(): string {
    this.#instanceCount++;
    return `instance-${this.#instanceCount}`;
  }
}

console.log(MathUtils.PI);
console.log(MathUtils.clamp(150, 0, 100));  // 100
console.log(MathUtils.create());             // instance-1

10. 枚举与字面量

10.1 数字枚举与字符串枚举

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

// 字符串枚举:不会自增,每个值必须显式指定
enum HttpMethod {
  GET = "GET",
  POST = "POST",
  PUT = "PUT",
  DELETE = "DELETE",
}

// 用在哪里
function request(url: string, method: HttpMethod): void {
  console.log(`${method} ${url}`);
}
request("/api/users", HttpMethod.GET);

10.2 const enum(编译时内联,零运行时开销)

const enum LogLevel {
  Debug = 0,
  Info = 1,
  Warn = 2,
  Error = 3,
}

const level = LogLevel.Info;
// 编译后 JS: const level = 1;  直接内联,没有对象生成

10.3 字面量联合类型 —— 枚举的替代方案

// 很多团队更倾向用字面量联合而非 enum
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type LogLevel = "debug" | "info" | "warn" | "error";

function log(level: LogLevel, message: string): void {
  const prefix = `[${level.toUpperCase()}]`;
  console.log(`${prefix} ${message}`);
}

log("info", "服务启动");    // ✅
// log("verbose", "...");   // ❌

// 配合 as const + typeof 从对象推导
const LOG_LEVELS = {
  DEBUG: "debug",
  INFO: "info",
  WARN: "warn",
  ERROR: "error",
} as const;

type LogLevelFromObject = (typeof LOG_LEVELS)[keyof typeof LOG_LEVELS];
// "debug" | "info" | "warn" | "error"

11. 类型守卫与类型收窄

11.1 typeof 守卫

function process(value: string | number | boolean): string {
  if (typeof value === "string") {
    return value.toUpperCase();    // value: string
  }
  if (typeof value === "number") {
    return value.toFixed(2);       // value: number
  }
  return value ? "true" : "false"; // value: boolean
}

11.2 instanceof 守卫

class ApiError extends Error {
  constructor(
    message: string,
    public statusCode: number,
  ) {
    super(message);
    this.name = "ApiError";
  }
}

class NetworkError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "NetworkError";
  }
}

function handleError(err: Error): string {
  if (err instanceof ApiError) {
    return `API 错误 [${err.statusCode}]: ${err.message}`;
  }
  if (err instanceof NetworkError) {
    return `网络错误: ${err.message}`;
  }
  return `未知错误: ${err.message}`;
}

11.3 自定义类型守卫(is 关键字)

interface Cat {
  meow(): void;
  name: string;
}
interface Dog {
  bark(): void;
  breed: string;
}

type Pet = Cat | Dog;

// 自定义守卫函数:返回 "x is Type"
function isCat(pet: Pet): pet is Cat {
  return "meow" in pet;
}

function isDog(pet: Pet): pet is Dog {
  return (pet as Dog).bark !== undefined;
}

// 使用
function interact(pet: Pet): void {
  if (isCat(pet)) {
    pet.meow();        // ✅ pet 收窄为 Cat
  } else {
    pet.bark();        // ✅ pet 收窄为 Dog
  }
}

11.4 in 运算符收窄

interface Fish {
  swim(): void;
}
interface Bird {
  fly(): void;
}

function move(animal: Fish | Bird): void {
  if ("swim" in animal) {
    animal.swim();     // animal: Fish
  } else {
    animal.fly();      // animal: Bird
  }
}

11.5 断言函数(Asserts)

// asserts 告知 TypeScript 此函数如果正常返回,则条件一定成立
function assert(condition: unknown, message?: string): asserts condition {
  if (!condition) {
    throw new Error(message ?? "断言失败");
  }
}

function getConfig(): { port: number } | undefined {
  return Math.random() > 0.5 ? { port: 8080 } : undefined;
}

const config = getConfig();
assert(config !== undefined, "配置不能为空");
console.log(config.port);  // ✅ config 现在一定不是 undefined
// 如果没有 assert,上面这行会报错 "config 可能为 undefined"

11.6 satisfies 运算符(TypeScript 4.9+)

// satisfies: 检查值是否符合类型,但保留最窄的推导类型

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
} satisfies Record<string, string | number[]>;
//                                       ↑ 检查每个值都是 string 或 number[]

// palette.red 的类型是 [255, 0, 0] — 保留了元组,没有被拓宽为 (string | number[])[]
palette.red[0];  // ✅ 255

// 不用 satisfies 的话:
// const palette: Record<string, string | number[]> = { ... }
// palette.red[0]  // ❌ 报错,因为 red 被拓宽为 string | number[]

12. 工具类型(Utility Types)

TypeScript 内置了大量实用工具类型。

12.1 对象操作

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Partial<T> — 全部可选
type UpdateUser = Partial<User>;

// Required<T> — 全部必填
type StrictUser = Required<User>;

// Pick<T, K> — 选取部分字段
type UserPublic = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }

// Omit<T, K> — 排除部分字段
type UserWithoutPassword = Omit<User, "password">;
// { id: number; name: string; email: string; createdAt: Date }

// Readonly<T> — 全部只读
type ReadonlyUser = Readonly<User>;

12.2 记录与排除

// Record<K, V> — 构造键值对对象
type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;

const perms: Permissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

// Exclude<T, U> — 从联合中排除
type NonAdminRoles = Exclude<Role, "admin">;  // "user" | "guest"

// Extract<T, U> — 从联合中提取
type OnlyLoading = Extract<"idle" | "loading" | "success", "loading">;  // "loading"

12.3 函数类型工具

// Parameters<T> — 提取函数参数类型
function login(username: string, password: string, remember: boolean): void {}
type LoginParams = Parameters<typeof login>;
// [username: string, password: string, remember: boolean]

// ReturnType<T> — 提取函数返回类型
function getUser() {
  return { name: "张三", age: 25, role: "admin" as const };
}
type UserFromFn = ReturnType<typeof getUser>;
// { name: string; age: number; role: "admin" }

// Awaited<T> — 提取 Promise 内部类型
type UserPromise = Promise<{ name: string }>;
type ResolvedUser = Awaited<UserPromise>;  // { name: string }

12.4 字符串操作(TypeScript 4.1+)

// 内置的字符串操作类型
type EventType = "mouseclick" | "keydown" | "formsubmit";

// Uppercase / Lowercase / Capitalize / Uncapitalize
type UpperEvent = Uppercase<EventType>;
// "MOUSECLICK" | "KEYDOWN" | "FORMSUBMIT"

type CapitalizedEvent = Capitalize<EventType>;
// "Mouseclick" | "Keydown" | "Formsubmit"

// 组合使用
type EventHandler = `on${Capitalize<EventType>}`;
// "onMouseclick" | "onKeydown" | "onFormsubmit"

12.5 NonNullable

// 从联合类型中排除 null 和 undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;  // string

// 常用场景:过滤数组中的空值
const items: (string | null | undefined)[] = ["a", null, "b", undefined, "c"];
const safeItems: string[] = items.filter((x): x is string => x != null);

13. 模块与声明文件

13.1 ES 模块

// math.ts — 导出
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

export type Vector2D = { x: number; y: number };

export default class Calculator {
  add(a: number, b: number): number { return a + b; }
}

// app.ts — 导入
import Calculator, { add, PI, type Vector2D } from "./math";

const calc = new Calculator();
console.log(add(1, 2));

// 只导入类型(编译后被完全移除)
import type { Vector2D } from "./math";

13.2 声明文件(.d.ts)

// types.d.ts — 全局类型声明文件

// 声明一个没有类型定义的 npm 模块
declare module "my-untyped-lib" {
  export function doSomething(input: string): number;
  export const version: string;
}

// 扩展全局变量
declare global {
  interface Window {
    __APP_VERSION__: string;
    __INITIAL_STATE__: Record<string, unknown>;
  }

  // 声明全局函数
  function gtag(event: string, params: Record<string, unknown>): void;
}

export {};  // 确保这是一个模块文件

13.3 声明第三方库的类型

// 给已有的 JS 库补充类型声明
// react-query 的类型声明示例

declare module "@tanstack/react-query" {
  export function useQuery<TData>(
    key: string[],
    fetcher: () => Promise<TData>,
    options?: {
      enabled?: boolean;
      staleTime?: number;
      retry?: number;
    }
  ): {
    data: TData | undefined;
    isLoading: boolean;
    isError: boolean;
    error: Error | null;
  };
}

14. 异步类型(Promise / async)

14.1 Promise 泛型

// Promise<T> — T 是 resolve 的值的类型
function fetchUser(id: number): Promise<{ name: string; age: number }> {
  return fetch(`/api/users/${id}`).then((res) => res.json());
}

// async 函数自动包裹返回值为 Promise
async function getUserName(id: number): Promise<string> {
  const user = await fetchUser(id);
  return user.name;
}

14.2 带错误处理的类型

// 定义 API 响应的联合类型
type Result<T> =
  | { ok: true; data: T }
  | { ok: false; error: string; code: number };

async function safeFetch<T>(url: string): Promise<Result<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) {
      return { ok: false, error: res.statusText, code: res.status };
    }
    const data: T = await res.json();
    return { ok: true, data };
  } catch (e) {
    return {
      ok: false,
      error: e instanceof Error ? e.message : "未知错误",
      code: 0,
    };
  }
}

// 使用(类型安全,不会漏处理错误)
const result = await safeFetch<User[]>("/api/users");
if (result.ok) {
  result.data.forEach((user) => console.log(user.name));
} else {
  console.error(`请求失败 [${result.code}]: ${result.error}`);
}

14.3 AbortController 的类型安全封装

interface FetchOptions extends RequestInit {
  timeout?: number;
}

async function fetchWithTimeout(
  url: string,
  options: FetchOptions = {}
): Promise<Response> {
  const { timeout = 10000, ...fetchOptions } = options;
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...fetchOptions,
      signal: controller.signal,
    });
    return response;
  } finally {
    clearTimeout(timer);
  }
}

15. 实战:类型安全的 Todo 应用

这是一个完整的 TypeScript 实战项目,涵盖了本教程的核心知识点。

// ==================== 类型定义 ====================

type Priority = "low" | "medium" | "high";
type TodoStatus = "todo" | "in-progress" | "done";

interface Todo {
  readonly id: string;
  title: string;
  description: string;
  status: TodoStatus;
  priority: Priority;
  tags: string[];
  createdAt: Date;
  updatedAt: Date;
}

// 创建和更新用的 DTO(Data Transfer Object)
type CreateTodoInput = Pick<Todo, "title" | "description" | "priority" | "tags">;
type UpdateTodoInput = Partial<Pick<Todo, "title" | "description" | "status" | "priority" | "tags">>;

// 查询过滤
interface TodoFilter {
  status?: TodoStatus;
  priority?: Priority;
  tag?: string;
  keyword?: string;
}

// ==================== 数据层 ====================

class TodoStore {
  private todos: Map<string, Todo> = new Map();

  create(input: CreateTodoInput): Todo {
    const now = new Date();
    const todo: Todo = {
      ...input,
      id: crypto.randomUUID(),
      status: "todo",
      createdAt: now,
      updatedAt: now,
    };
    this.todos.set(todo.id, todo);
    return todo;
  }

  getById(id: string): Todo | undefined {
    return this.todos.get(id);
  }

  update(id: string, input: UpdateTodoInput): Todo | null {
    const existing = this.todos.get(id);
    if (!existing) return null;

    const updated: Todo = {
      ...existing,
      ...input,
      updatedAt: new Date(),
    };
    this.todos.set(id, updated);
    return updated;
  }

  delete(id: string): boolean {
    return this.todos.delete(id);
  }

  getAll(filter?: TodoFilter): Todo[] {
    let items = [...this.todos.values()];

    if (filter?.status) {
      items = items.filter((t) => t.status === filter.status);
    }
    if (filter?.priority) {
      items = items.filter((t) => t.priority === filter.priority);
    }
    if (filter?.tag) {
      items = items.filter((t) => t.tags.includes(filter.tag!));
    }
    if (filter?.keyword) {
      const kw = filter.keyword.toLowerCase();
      items = items.filter(
        (t) =>
          t.title.toLowerCase().includes(kw) ||
          t.description.toLowerCase().includes(kw)
      );
    }

    return items.sort(
      (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
    );
  }

  getStats(): { total: number; done: number; byPriority: Record<Priority, number> } {
    const all = [...this.todos.values()];
    return {
      total: all.length,
      done: all.filter((t) => t.status === "done").length,
      byPriority: {
        low: all.filter((t) => t.priority === "low").length,
        medium: all.filter((t) => t.priority === "medium").length,
        high: all.filter((t) => t.priority === "high").length,
      },
    };
  }
}

// ==================== 工具函数 ====================

const PRIORITY_LABELS: Record<Priority, string> = {
  low: "🟢 低",
  medium: "🟡 中",
  high: "🔴 高",
};

const STATUS_LABELS: Record<TodoStatus, string> = {
  "todo": "📋 待办",
  "in-progress": "🔄 进行中",
  "done": "✅ 完成",
};

function formatTodo(todo: Todo): string {
  const lines = [
    `[${todo.id.slice(0, 8)}] ${todo.title}`,
    `  优先级: ${PRIORITY_LABELS[todo.priority]} | 状态: ${STATUS_LABELS[todo.status]}`,
    `  描述: ${todo.description || "(无)"}`,
    `  标签: ${todo.tags.length > 0 ? todo.tags.join(", ") : "(无)"}`,
    `  创建: ${todo.createdAt.toLocaleString()}`,
  ];
  return lines.join("\n");
}

// ==================== 使用示例 ====================

// 单例
const store = new TodoStore();

// 创建
const todo1 = store.create({
  title: "学习 TypeScript 泛型",
  description: "完成泛型章节的练习",
  priority: "high",
  tags: ["学习", "TypeScript"],
});

store.create({
  title: "写周报",
  description: "总结本周工作",
  priority: "medium",
  tags: ["工作"],
});

store.create({
  title: "跑步",
  description: "3公里",
  priority: "low",
  tags: ["运动"],
});

// 更新
store.update(todo1.id, { status: "in-progress" });

// 查询
console.log("--- 高优先级任务 ---");
store.getAll({ priority: "high" }).forEach((t) => console.log(formatTodo(t)));

console.log("\n--- 所有任务 ---");
store.getAll().forEach((t) => console.log(formatTodo(t)));

// 统计
const stats = store.getStats();
console.log("\n--- 统计 ---");
console.log(`总数: ${stats.total}, 完成: ${stats.done}`);
console.log(`低/中/高: ${stats.byPriority.low}/${stats.byPriority.medium}/${stats.byPriority.high}`);

继续学习路线

  1. zod — 运行时数据校验,生成 TypeScript 类型
  2. ts-pattern — 库级别的模式匹配,比 switch 更强大
  3. @tanstack/react-query — 服务端状态管理的类型实践
  4. Zustand — 极致简洁的状态管理,TS 体验一流
  5. tRPC — 端到端类型安全的 API 方案
  6. Effect-TS — 函数式编程 + 类型级错误处理
  7. type-challenges — GitHub 上的 TS 类型体操练习题

更多推荐