Skip to content

第 7 章:反模式 —— 你可能正在犯的错误

在前面的章节中,我们探讨了 interfacetype 的正确使用方式。然而,在实际项目中,开发者经常会陷入一些反模式。在这一章中,我们将识别并分析这些常见的错误,帮助你写出更好的类型定义。

盲目 Omit<yargs.CommandModule, 'command'>

一个常见的反模式是盲目使用 Omit 来去除某些属性,而不考虑实际需求:

typescript
// 反模式:盲目使用 Omit
import yargs from 'yargs';

type CustomCommand = Omit<yargs.CommandModule, 'command'> & {
  command: string | string[];
};

// 更好的做法:明确你需要的属性
type CustomCommand = {
  command: string | string[];
  describe?: string | false;
  builder?: (args: yargs.Argv) => yargs.Argv;
  handler: (args: yargs.Arguments) => void | Promise<void>;
};

盲目的类型操作会导致类型定义过于宽泛或丢失重要的类型信息。更好的做法是明确你需要哪些属性,并重新定义类型。

为了"可扩展"用 interface,结果没人扩展

另一种常见的反模式是为了潜在的扩展性而使用 interface,但实际上从未被扩展:

typescript
// 反模式:过度设计
interface UserProfile {
  name: string;
  email: string;
}

// 在整个项目中只有这一个实现,从未被扩展过

// 更好的做法:使用 type
type UserProfile = {
  name: string;
  email: string;
};

如果你的类型只有一种用途且从未被扩展,那么使用 type 更加合适。过度设计只会增加不必要的复杂性。

在 DTO 中用 interface 导致意外合并

在定义 DTO(Data Transfer Object)时使用 interface 可能会导致意外的声明合并:

typescript
// 反模式:在不同文件中意外定义了同名 interface
// file1.ts
interface UserResponse {
  id: number;
  name: string;
}

// file2.ts
interface UserResponse {
  email: string;
}

// 结果合并后的 UserResponse 包含了所有属性
// 如果这不是你想要的,就会导致问题

// 更好的做法:使用 type
// file1.ts
type UserResponse = {
  id: number;
  name: string;
};

// file2.ts
type UserResponse = {
  email: string;
}; // 这里会报错,防止意外合并

在定义数据传输对象时,通常不希望发生声明合并,因为这可能会引入非预期的属性。

混淆交叉类型和继承

有时候开发者会混淆交叉类型(Intersection Types)和接口继承:

typescript
// 可能引起混淆的写法
interface Person {
  name: string;
}

interface Employee {
  employeeId: number;
}

// 方式1:交叉类型
type WorkingPerson = Person & Employee;

// 方式2:接口继承
interface WorkingPerson extends Person, Employee {}

// 这两种方式看似相同,但在某些情况下会有差异

虽然在大多数情况下这两种方式效果相似,但在处理同名属性、函数重载等复杂情况时可能会有细微差别。

过度嵌套的类型定义

另一个常见问题是过度嵌套的类型定义,使得类型难以理解和维护:

typescript
// 反模式:过度嵌套
type ComplexType = {
  user: {
    profile: {
      personal: {
        name: string;
        address: {
          street: string;
          city: string;
          country: string;
        };
      };
      professional: {
        company: string;
        position: string;
        skills: string[];
      };
    };
  };
};

// 更好的做法:分解为小的、可重用的类型
type Address = {
  street: string;
  city: string;
  country: string;
};

type PersonalInfo = {
  name: string;
  address: Address;
};

type ProfessionalInfo = {
  company: string;
  position: string;
  skills: string[];
};

type UserProfile = {
  personal: PersonalInfo;
  professional: ProfessionalInfo;
};

type UserType = {
  user: {
    profile: UserProfile;
  };
};

分解类型不仅可以提高可读性,还可以促进类型重用。

忽视联合类型的完备性检查

在使用联合类型时,容易忽视对所有情况的处理:

typescript
// 反模式:没有处理所有情况
type Status = 'loading' | 'success' | 'error';

interface LoadingState {
  status: 'loading';
}

interface SuccessState {
  status: 'success';
  data: any;
}

interface ErrorState {
  status: 'error';
  error: Error;
}

type AppState = LoadingState | SuccessState | ErrorState;

// 忘记处理 error 状态
function renderState(state: AppState) {
  switch (state.status) {
    case 'loading':
      return 'Loading...';
    case 'success':
      return state.data;
    // 忘记处理 'error' 情况,TypeScript 不会警告!
  }
}

// 更好的做法:使用类型守卫确保完备性
function renderStateComplete(state: AppState) {
  switch (state.status) {
    case 'loading':
      return 'Loading...';
    case 'success':
      return state.data;
    case 'error':
      return `Error: ${state.error.message}`;
    default:
      // 这里使用 never 类型确保所有情况都被处理
      const exhaustiveCheck: never = state;
      throw new Error(`Unhandled state: ${exhaustiveCheck}`);
  }
}

使用 never 类型可以帮助 TypeScript 检查是否处理了所有可能的情况。

小结

在本章中,我们探讨了几种常见的反模式:

  1. 盲目使用 Omit 等类型操作符
  2. 为了潜在扩展性而过度使用 interface
  3. 在 DTO 中使用 interface 导致意外合并
  4. 混淆交叉类型和接口继承
  5. 过度嵌套的类型定义
  6. 忽视联合类型的完备性检查

避免这些反模式可以让你的类型定义更加清晰、健壮和易于维护。

下一章,我们将提供一个可视化的决策树,帮助你在面对具体选择时快速做出判断。


思考题: 你在项目中是否遇到过上述提到的反模式?你是如何发现并解决这些问题的?