前端开发··56 阅读·预计 14 分钟

TypeScript 高级类型体操:从入门到实战的完整指南

背景

TypeScript 已成为前端开发的标配工具,但很多开发者只停留在基础类型定义层面,对高级类型特性如泛型约束、条件类型、映射类型等知之甚少。掌握这些高级技巧,不仅能提升代码的类型安全性,还能减少大量重复的运行时校验代码。

本文将带你从基础概念出发,通过实战案例逐步掌握 TypeScript 高级类型编程技巧。

核心概念

1. 泛型约束(Generic Constraints)

泛型是 TypeScript 类型系统的基石,但单纯使用泛型往往不够,我们需要对泛型参数进行约束。

// 基础泛型
function identity<T>(arg: T): T {
  return arg;
}

// 泛型约束:限制 T 必须有 length 属性
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("hello"); // ✅ 字符串有 length
logLength([1, 2, 3]); // ✅ 数组有 length
logLength(123); // ❌ 数字没有 length,编译报错

2. 条件类型(Conditional Types)

条件类型允许根据类型关系进行逻辑判断,是构建复杂类型的利器。

// 基础条件类型
type IsString<T> = T extends string ? true : false;

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

// 实战案例:提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type R = ReturnType<() => string>; // string
type R2 = ReturnType<(x: number) => boolean>; // boolean

3. 映射类型(Mapped Types)

映射类型可以批量转换类型的属性,是构建工具类型的核心。

// 将所有属性变为可选
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

// 将所有属性变为只读
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

// 实战:深度只读
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>
 : T[P];
};

interface User {
  name: string;
  profile: {
    age: number;
    address: {
      city: string;
    };
  };
}

type ReadonlyUser = DeepReadonly<User>;
// profile 和 address 都变成了只读

实战案例

案例 1:类型安全的 API 请求封装

// 定义 API 响应类型
interface ApiResponse<T> {
  code: number;
  data: T;
  message: string;
}

// 定义请求参数类型
type QueryParams = Record<string, string | number | boolean>;

// 类型安全的请求函数
async function request<T>(
  url: string,
  options?: {
    method?: "GET" | "POST" | "PUT" | "DELETE";
    body?: object;
    query?: QueryParams;
  }
): Promise<ApiResponse<T>> {
  const response = await fetch(url, {
    method: options?.method || "GET",
    headers: { "Content-Type": "application/json" },
    body: options?.body ? JSON.stringify(options.body) : undefined,
  });
  return response.json();
}

// 使用示例
interface UserInfo {
  id: string;
  name: string;
  email: string;
}

// 自动推断返回类型
const result = await request<UserInfo>("/api/user/123");
console.log(result.data.name); // 类型安全!

案例 2:自动生成表单类型

// 从数据类型生成表单类型
type FormType<T> = {
  [P in keyof T]: T[P] extends string
    ? string | undefined
    : T[P] extends number
    ? number | undefined
    : T[P];
};

interface UserData {
  id: number;
  name: string;
  email: string;
  age: number;
}

// 表单类型:所有字段都可选
type UserForm = FormType<Partial<UserData>>;

// 验证函数类型
type Validator<T> = (value: T[keyof T]) => boolean | string;

function createForm<T>(
  initialValues: FormType<T>,
  validators?: Partial<Record<keyof T, Validator<T>>>
) {
  // 表单逻辑实现
  return {
    values: initialValues,
    validate: () => true,
  };
}

案例 3:事件系统类型推导

// 定义事件映射
interface EventMap {
  click: { x: number; y: number };
  scroll: { scrollTop: number };
  input: { value: string };
}

// 类型安全的事件发射器
class TypedEventEmitter<T extends Record<string, object>> {
  private listeners = new Map<keyof T, Set<Function>>();

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);
  }

  emit<K extends keyof T>(event: K, data: T[K]) {
    const set = this.listeners.get(event);
    set?.forEach((fn) => fn(data));
  }
}

// 使用
const emitter = new TypedEventEmitter<EventMap>();

emitter.on("click", (data) => {
  console.log(data.x, data.y); // 类型自动推导!
});

emitter.emit("click", { x: 100, y: 200 }); // ✅
emitter.emit("click", { x: "wrong" }); // ❌ 类型错误

最佳实践

1. 善用工具类型组合

// 组合多个工具类型
type OptionalExceptId<T extends { id: unknown }> = Omit<T, "id"> & {
  id: T["id"];
} & Partial<Omit<T, "id">>;

interface Item {
  id: string;
  name: string;
  price: number;
}

// 更新时:id 必填,其他可选
type UpdateItem = OptionalExceptId<Item>;

2. 使用 infer 提取类型

// 提取 Promise 值类型
type PromiseValue<T> = T extends Promise<infer V> ? V : T;

type Result = PromiseValue<Promise<string>>; // string

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

type Elem = ArrayElement<string[]>; // string

3. 类型守卫配合类型谓词

interface Fish { swim: () => void }
interface Bird { fly: () => void }

function isFish(pet: Fish | Bird): pet is Fish {
  return "swim" in pet;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim(); // 类型收窄为 Fish
  } else {
    pet.fly(); // 类型收窄为 Bird
  }
}

常见问题

Q1: any 和 unknown 有什么区别?

let a: any = "hello";
a.foo(); // 编译通过,运行时报错 ❌

let u: unknown = "hello";
u.foo(); // 编译报错 ✅

if (typeof u === "string") {
  u.toUpperCase(); // 安全使用
}

结论:优先使用 unknown,避免使用 any

Q2: 什么时候用 type,什么时候用 interface?

  • interface:定义对象形状、需要扩展/合并时
  • type:联合类型、交叉类型、工具类型时
// interface 可扩展
interface Animal { name: string }
interface Animal { age: number } // 合并

// type 更灵活
type ID = string | number;
type Point = { x: number } & { y: number };

Q3: 如何处理复杂的动态键值?

// 使用 Record 和模板字面量类型
type EventName = `on${Capitalize<string>}`;
type Handlers = Record<EventName, Function>;

// 更精确的动态键
type Actions = "create" | "update" | "delete";
type ActionHandlers = {
  [K in Actions as `${K}Handler`]: () => void;
};
// 结果: { createHandler: () => void; updateHandler: () => void; ... }

总结

TypeScript 高级类型虽然学习曲线较陡,但掌握后能显著提升代码质量:

  1. 类型安全:在编译时捕获更多错误
  2. 智能提示:IDE 自动补全更准确
  3. 代码文档:类型即文档,自解释性强
  4. 重构友好:修改类型即知影响范围

建议从日常开发中的痛点出发,逐步引入高级类型技巧。不要过度设计,够用就好。记住:类型系统是工具,不是目的。


推荐资源

  • TypeScript 官方文档
  • TypeScript Deep Dive(在线免费书籍)
  • GitHub 上的 type-challenges 练习题
0 评论

评论区

登录 后参与评论