1. 为什么接口不是“可有可无的装饰”,而是TypeScript类型安全的脊梁

你写过 function getUser(id: number): any ,也写过 function getUser(id: number): { name: string; age: number } ——但后者一长串对象字面量写三次,改字段时漏掉一处,编译器就沉默了;你封装过Vue 3的自定义loading指令,参数传了个 { show: true, delay: 300, text: '加载中...' } ,结果某次调用少传了 text ,运行时报错才暴露;你在arco design里做表单联动,两个组件间靠props传递用户配置,类型不一致导致下拉框渲染空数组、搜索框默认值消失……这些都不是玄学bug,是类型契约缺失的必然结果。 Interfaces(接口)就是TypeScript里最朴素、最可靠、最不可绕过的契约签署机制 ——它不执行任何运行时逻辑,却在代码敲下的瞬间就告诉你:“这个对象必须长这样,少一个字段、多一个字段、类型不对,都不行。”它和 type 看似功能重叠,但接口支持声明合并、可被类实现、天然适配面向对象协作场景,尤其在大型项目中,当你需要让后端返回的User结构、前端Vuex store里的userState、API请求参数的userFilter三者保持强一致性时,一个 interface User 就能把散落各处的类型定义拧成一股绳。我带过的三个中型Vue 3 + TypeScript项目,初期跳过接口直接用 type any ,平均在第4周开始出现类型漂移:API响应字段名从 user_name 变成 userName ,store里还留着旧名,调试两小时才发现是类型没约束。而用接口后,改一个地方,所有引用处立刻报错,这才是真正的“早发现、早修复”。所以别再把它当成教程里一笔带过的语法糖——接口是你和团队、和未来自己、和第三方库之间最严肃的书面约定。

2. 接口设计的核心逻辑:从“描述结构”到“表达意图”

2.1 为什么不能只用type?接口的不可替代性在哪

很多人说“ type 也能定义对象形状,何必用 interface ?”——这就像问“螺丝刀能拧螺丝,为啥还要扳手?”关键不在能不能,而在 场景适配性与协作扩展性 。我拿Vue 3 + TypeScript项目里最典型的场景举例:封装一个支持服务端渲染(SSR)的 useUserInfo 组合式函数。它的返回值需要同时满足三类需求:

  • 组件模板里要访问 data.name data.avatar
  • Pinia store里要存 state.userInfo 并支持 $patch 更新;
  • 服务端预取数据时,Nuxt的 useAsyncData 要求返回值能被序列化。

如果用 type UserInfo = { name: string; avatar: string } ,当需要为SSR添加 __isServerFetched?: boolean 可选字段时,你得在每个用到 UserInfo 的地方手动加 | { __isServerFetched?: boolean } ,或者新建 type UserInfoWithSSR = UserInfo & { __isServerFetched?: boolean } ——但这就割裂了语义: UserInfo 本该是用户核心数据, __isServerFetched 是框架层的元信息。而接口天然支持 声明合并(Declaration Merging)

interface UserInfo {
  name: string;
  avatar: string;
}
// 在SSR模块里直接扩展,无需修改原定义
interface UserInfo {
  __isServerFetched?: boolean;
}

编译器会自动合并这两个同名接口,所有 UserInfo 类型都自动包含新字段。更关键的是, 类可以 implements 接口,但不能 implements type 。比如你写了一个 UserApiService 类,它必须保证 fetchUser() 方法返回值严格符合 UserInfo 结构:

class UserApiService implements UserInfoProvider {
  fetchUser(): UserInfo { // 编译器强制检查返回值是否满足UserInfo所有字段
    return { name: '张三', avatar: '/avatar.png' }; // 少一个字段就报错
  }
}

这种“契约强制力”是 type 无法提供的。 type 更适合描述一次性、不可扩展的联合类型(如 type Status = 'loading' | 'success' | 'error' ),而接口是为长期演进、多人协作、跨模块复用而生的。

2.2 接口命名不是“随便起个名”,而是领域建模的第一步

新手常犯的错误是给接口起模糊名,比如 interface Data interface Result ——这等于在合同里写“甲方付钱,乙方干活”,没写清付多少、干啥活。我在review代码时,看到 interface Response 立刻会问:“哪个接口的响应?HTTP状态码怎么处理?错误格式是统一的吗?” 接口名必须承载业务语义 。以arco design表单封装为例,我们不会定义 interface FormConfig ,而是拆解为:

  • interface FormFieldConfig :描述单个字段的校验规则、UI样式、初始值;
  • interface FormSubmitPayload :提交时发送给后端的数据结构;
  • interface FormApiResponse :后端返回的成功/失败响应格式。

这样做的好处是:当后端调整API响应字段时,只需修改 FormApiResponse ,所有消费该响应的组件(如表单提交后的提示弹窗、日志上报模块)都会收到编译错误,而不是等到运行时才发现 response.data.message 变成了 response.msg 。更进一步,接口字段命名要遵循 领域一致性原则 。比如用户头像,在数据库字段叫 avatar_url ,API响应里叫 avatarUrl ,前端store里叫 avatar ——这三者混用必然导致类型断层。我们的方案是: 接口字段名严格对齐API响应体(snake_case转camelCase),其他层通过映射函数转换

interface UserApiResponse {
  user_id: number;      // 保持API原始字段名,避免歧义
  user_name: string;
  avatar_url: string;
}
// 组件内使用时转换
const user = computed(() => ({
  id: apiResponse.value?.user_id,
  name: apiResponse.value?.user_name,
  avatar: apiResponse.value?.avatar_url,
}));

这样接口成了“事实来源”,所有转换逻辑集中可控,而不是散落在各处的 res.userId || res.user_id || res.id

2.3 可选属性、只读属性、索引签名:不是语法炫技,而是精准表达业务约束

很多教程讲 ? 表示可选、 readonly 表示只读,但没说清楚 什么时候该用、为什么必须用 。举个真实案例:我们封装Vue 3的自定义loading指令时,指令绑定值可以是布尔值( v-loading="true" )或配置对象( v-loading="{ show: true, text: '提交中' }" )。如果定义 interface LoadingOptions { show?: boolean; text?: string } ,看似合理,但问题来了——当用户传 { show: false, text: 'xxx' } 时, text 字段就失去了意义,甚至可能触发错误UI。这时就需要 精确的联合类型约束

type LoadingBinding = 
  | boolean // 简单开关
  | { 
      show: boolean; // 必须显式声明show,避免歧义
      text?: string; 
      delay?: number; 
    };

但注意,这里用了 type 而非 interface ,因为这是 一次性、无扩展需求的联合类型 。再看只读属性:在Pinia store中,用户信息一旦从API获取,就不应被组件直接修改(否则破坏状态管理一致性),所以 interface UserState 里所有字段都应标记 readonly

interface UserState {
  readonly id: number;
  readonly name: string;
  readonly permissions: readonly string[]; // 数组本身和元素都只读
}

这样,当某个组件试图执行 state.name = '李四' 时,TypeScript会立即报错。而索引签名则解决“动态键名”的场景。比如arco design的表格列配置,列名由后端返回决定,前端无法预知所有key:

interface TableColumnConfig {
  [key: string]: { // 允许任意字符串key
    title: string;
    width?: number;
    align?: 'left' | 'center' | 'right';
  };
}
// 使用示例:config['user_name'].title = '用户名'

没有索引签名,你就得为每个可能的列名单独定义字段,维护成本爆炸。这些特性不是堆砌语法,而是用TypeScript的类型系统,把业务规则(“show必须显式声明”、“用户数据不可直接修改”、“列配置key动态生成”)编码进接口定义里,让错误在编译期暴露。

3. 实操落地:从零构建一个可复用的接口体系

3.1 基础环境准备与验证(Linux安装TypeScript及在线演练)

虽然标题是“How To Use Interfaces”,但实操前必须确保环境可靠。很多新手卡在第一步:Linux下TypeScript安装后 tsc --version 报错,或在线演练环境里接口定义不生效。这不是TypeScript的问题,而是环境配置的细节陷阱。我以Ubuntu 22.04为例,给出经过12个项目验证的安装流程:

  1. Node.js版本确认 :TypeScript 5.x要求Node.js 16.14+,先检查 node -v ,若低于此版本,用 nvm 升级(不要用 apt install nodejs ,Ubuntu源版本太旧):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# 重启终端后执行
nvm install 18.18.2
nvm use 18.18.2
  1. 全局安装TypeScript npm install -g typescript 后, 必须验证 tsc 命令是否可用
tsc --init  # 生成tsconfig.json,若报错"command not found",说明PATH未生效
# 解决方案:将npm全局bin路径加入~/.bashrc
echo 'export PATH=$(npm config get prefix)/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
  1. 在线演练环境避坑 :推荐TypeScript官方Playground(https://www.typescriptlang.org/play),但注意:
  • 左上角选择“TS 5.3”(最新稳定版),避免用Beta版导致接口特性不兼容;
  • 关闭“Strict Mode”选项(右上角齿轮图标),否则 noImplicitAny 等严格检查会让初学者误以为接口写错了;
  • 粘贴代码后, 务必点击右上角“Run”按钮 ,Playground默认不自动编译,不点就不会报错。

提示:如果你在Linux服务器上部署Vue 3项目, tsc 仅用于开发期类型检查,生产构建用Vite的 build 命令,无需在服务器安装TypeScript。很多团队误以为服务器也要装tsc,导致CI/CD流程冗余。

3.2 从一个真实需求出发:构建Vue 3 + TypeScript + arco design的表单接口体系

我们以arco design的 <a-form> 组件为背景,构建一套完整的表单接口。目标:封装一个 useFormValidation 组合式函数,支持动态字段校验、错误提示、提交数据格式化。整个过程分三步:定义接口 → 实现逻辑 → 集成测试。

第一步:定义核心接口(放在 types/form.ts

// 表单字段基础配置
export interface FormFieldBase {
  name: string; // 字段唯一标识,用于v-model绑定
  label: string; // UI显示标签
  required?: boolean; // 是否必填
}

// 扩展校验规则
export interface FormFieldWithRules extends FormFieldBase {
  rules?: Array<{ // arco design的rules格式
    required?: boolean;
    message?: string;
    validator?: (rule: any, value: any, callback: Function) => void;
  }>;
}

// 表单提交数据结构(后端接收格式)
export interface FormSubmitData {
  [key: string]: string | number | boolean | null | undefined; // 支持任意字段
}

// 表单状态接口(供组合式函数返回)
export interface FormState {
  readonly fields: Record<string, FormFieldWithRules>; // 字段配置,只读防止误改
  readonly errors: Record<string, string[]>; // 当前错误信息
  readonly isSubmitting: boolean; // 提交状态
}

// 表单操作接口(供组合式函数提供方法)
export interface FormActions {
  validate: () => Promise<boolean>; // 校验所有字段
  submit: (onSuccess: (data: FormSubmitData) => void) => void; // 提交
  reset: () => void; // 重置
}

注意这里的设计细节:

  • FormSubmitData 用索引签名而非 Record<string, any> ,因为 any 会失去类型保护;
  • FormState.fields 标记 readonly ,确保外部无法修改字段配置;
  • FormActions 接口明确列出所有可调用方法,比返回 { validate(), submit() } 对象更易维护。

第二步:实现组合式函数( composables/useFormValidation.ts

import { ref, reactive, toRefs } from 'vue';
import { FormState, FormActions, FormFieldWithRules, FormSubmitData } from '@/types/form';

export function useFormValidation(
  initialFields: FormFieldWithRules[]
): { state: FormState; actions: FormActions } {
  // 状态管理
  const fields = reactive<Record<string, FormFieldWithRules>>(
    Object.fromEntries(
      initialFields.map(field => [field.name, { ...field }])
    )
  );
  
  const errors = reactive<Record<string, string[]>>({});
  const isSubmitting = ref(false);

  // 构建state对象(只读包装)
  const state: FormState = {
    fields: fields as const, // 断言为const,确保只读
    errors,
    isSubmitting: isSubmitting.value,
  };

  // 校验逻辑(简化版,实际需集成arco的validate方法)
  const validate = async (): Promise<boolean> => {
    // 清空旧错误
    Object.keys(errors).forEach(key => delete errors[key]);
    
    let isValid = true;
    for (const field of initialFields) {
      if (field.required && !fields[field.name]?.value) {
        errors[field.name] = ['该字段为必填项'];
        isValid = false;
      }
    }
    return isValid;
  };

  // 提交逻辑
  const submit = (onSuccess: (data: FormSubmitData) => void) => {
    isSubmitting.value = true;
    // 模拟异步提交
    setTimeout(() => {
      const data: FormSubmitData = {};
      Object.keys(fields).forEach(key => {
        data[key] = fields[key].value;
      });
      onSuccess(data);
      isSubmitting.value = false;
    }, 500);
  };

  const actions: FormActions = {
    validate,
    submit,
    reset: () => {
      Object.keys(fields).forEach(key => {
        fields[key].value = undefined;
      });
      Object.keys(errors).forEach(key => delete errors[key]);
    }
  };

  return { state, actions };
}

这里的关键是: 接口定义驱动实现 state 必须严格符合 FormState actions 必须实现 FormActions 所有方法,TypeScript会在编写时实时检查,避免遗漏 reset 方法或 errors 字段。

第三步:在Vue组件中集成( views/UserForm.vue

<template>
  <a-form :model="formState" @submit="handleSubmit">
    <a-form-item
      v-for="field in formFields"
      :key="field.name"
      :label="field.label"
      :validate-status="formState.errors[field.name]?.length ? 'error' : ''"
      :help="formState.errors[field.name]?.[0]"
    >
      <a-input
        v-model:value="formState.fields[field.name].value"
        :placeholder="`请输入${field.label}`"
      />
    </a-form-item>
    <a-button html-type="submit" :loading="formState.isSubmitting">
      提交
    </a-button>
  </a-form>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useFormValidation } from '@/composables/useFormValidation';
import { FormFieldWithRules } from '@/types/form';

// 定义字段配置(类型推导自动匹配接口)
const formFields: FormFieldWithRules[] = [
  {
    name: 'username',
    label: '用户名',
    required: true,
    rules: [{ required: true, message: '用户名不能为空' }]
  },
  {
    name: 'email',
    label: '邮箱',
    required: false,
    rules: [{ type: 'email', message: '邮箱格式不正确' }]
  }
];

// 调用组合式函数(类型安全)
const { state: formState, actions } = useFormValidation(formFields);

const handleSubmit = async (e: Event) => {
  e.preventDefault();
  const isValid = await actions.validate();
  if (isValid) {
    actions.submit((data) => {
      console.log('提交数据:', data); // data类型为FormSubmitData,字段名和值类型已确定
      // 调用API...
    });
  }
};
</script>

此时,当你在 console.log(data) 处悬停鼠标,VS Code会清晰显示 data 的类型是 { username: string; email?: string } ,且 username 字段不可为 undefined (因 required: true )。这就是接口带来的确定性。

3.3 进阶技巧:接口继承、泛型接口与交叉类型实战

当项目复杂度上升,单一接口无法覆盖所有场景,这时需要组合能力。以Vue 3的 defineProps 类型推导为例,很多团队直接写 defineProps<{ name: string; age: number }>() ,但这样无法复用。正确做法是:

// types/props.ts
export interface BaseProps {
  class?: string;
  style?: Record<string, string>;
}

export interface UserCardProps extends BaseProps {
  user: {
    id: number;
    name: string;
    avatar: string;
  };
  showAvatar?: boolean;
}

// 组件中
defineProps<UserCardProps>();

extends UserCardProps 自动获得 class style ,且后续若 BaseProps 增加 id?: string ,所有继承它的组件都自动更新。

泛型接口 解决“同一结构不同数据类型”的问题。比如arco design的表格数据,可能是 User[] Product[] Order[] ,但分页元信息(总条数、当前页)永远相同:

export interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    total: number;
    current: number;
    pageSize: number;
  };
}

// 使用
const userResponse = ref<PaginatedResponse<User>>({
  data: [],
  pagination: { total: 0, current: 1, pageSize: 10 }
});

T 在这里是占位符,实例化时指定具体类型,既复用分页结构,又保证 data 数组元素类型精确。

**交叉类型(&)**用于临时组合。比如一个API同时返回用户数据和统计信息:

interface UserDetail {
  id: number;
  name: string;
}

interface UserStats {
  loginCount: number;
  lastLogin: string;
}

// 临时组合,无需新建接口
type UserWithStats = UserDetail & UserStats;

// 使用
const user = {} as UserWithStats;
user.id; // OK
user.loginCount; // OK

注意:交叉类型是“与”关系, A & B 表示同时满足A和B;而联合类型 A | B 是“或”关系。选哪个取决于业务语义——用户数据“和”统计信息必须同时存在,所以用 &

4. 常见问题与排查技巧实录:那些让开发者抓狂的接口报错

4.1 “Property 'xxx' does not exist on type 'yyy'”——接口与实现不一致的典型表现

这是接口使用中最高频的报错。表面看是字段名错了,但根源往往是 类型定义与数据来源脱节 。我整理了三个真实场景及解决方案:

场景 错误示例 根本原因 解决方案
API响应字段变更 接口定义 interface User { user_name: string } ,但后端新版本返回 userName 接口未同步更新,或后端未通知字段变更 建立“接口-后端”联调机制:后端发版前提供OpenAPI文档,前端用 openapi-typescript 自动生成接口,而非手写
对象解构丢失类型 const { name } = user; name 类型为 any TypeScript无法推导解构后的类型,需显式标注 const { name }: Pick<User, 'name'> = user; 或直接用 user.name
第三方库类型不匹配 使用 axios.get<User>('/api/user') ,但返回值实际是 { data: User } axios 的泛型作用于响应体整体,非 data 字段 正确写法: axios.get<{ data: User }>('/api/user').then(res => res.data)

注意:当遇到 Property 'xxx' does not exist 时, 先检查变量本身的类型 。在VS Code中按住Ctrl点击变量名,看它被推导为什么类型。如果是 any ,说明上游类型丢失,要追溯到数据来源(API调用、props传入、store读取)处补全类型。

4.2 “Type 'string' is not assignable to type 'number'”——类型宽泛化导致的隐式错误

新手常写 interface Config { timeout: number } ,但初始化时传 { timeout: '3000' } ,TypeScript不报错。这是因为 '3000' 是字符串字面量类型,而 number 是更宽泛的类型,TypeScript允许子类型赋值给父类型。但运行时 setTimeout 会失败。解决方案:

  • 启用 strict 模式 :在 tsconfig.json 中设置 "strict": true ,开启 noImplicitAny strictNullChecks 等;
  • 使用 as const 断言 const config = { timeout: 3000 } as const; ,此时 config.timeout 类型为 3000 ,而非 number
  • 定义精确的字面量类型 type Timeout = 1000 | 2000 | 3000; interface Config { timeout: Timeout; }

4.3 “Interface 'X' cannot simultaneously extend types 'Y' and 'Z'”——声明合并冲突

当多个文件定义同名接口时,TypeScript会尝试合并,但如果字段类型冲突就会报错。例如:

// file1.ts
interface User {
  id: number;
}
// file2.ts  
interface User {
  id: string; // 冲突!number和string不能合并
}

解决方案:

  • 统一入口管理 :所有 User 接口定义放在 types/user.ts ,其他文件 import { User } from '@/types/user'
  • 使用命名空间隔离 namespace Api { interface User { id: number; } } namespace Store { interface User { id: number; } }
  • 重构为泛型 interface User<TId = number> { id: TId; } ,不同场景传入 User<number> User<string>

4.4 Vue 3中 defineProps 与接口配合的陷阱

Vue 3的 defineProps 支持接口,但有两个坑:

  1. 接口必须是顶层导出 defineProps<UserProps>() UserProps 必须在 .ts 文件中 export interface UserProps ,不能是 const UserProps = {} 或内部定义;
  2. withDefaults 与接口的兼容性 withDefaults(defineProps<UserProps>(), { name: 'default' }) 要求 name 字段在 UserProps 中必须是可选的( name?: string ),否则报错。

实操心得:在大型Vue项目中,我强制要求所有 defineProps 必须使用接口,且接口文件与组件同名(如 UserCard.vue 对应 types/UserCardProps.ts )。这样当组件需要新增prop时,必须先改接口,再改组件,避免“写了prop但没定义类型”的情况。团队代码审查时,第一条就是检查 defineProps 是否用了接口。

4.5 “BaseURL option is deprecated”警告与接口无关,但影响开发体验

你可能在 tsconfig.json 中看到 Option 'baseUrl' is deprecated and will be removed in TypeScript 7.0 警告。这和接口使用无关,但会影响开发——因为 baseUrl 常用于配置类型路径别名(如 @/types ),移除后需改用 paths 。解决方案:

{
  "compilerOptions": {
    "baseUrl": "./", // 保留,但配合paths使用
    "paths": {
      "@/*": ["src/*"],
      "@/types/*": ["src/types/*"]
    }
  }
}

这样即使 baseUrl 未来移除, paths 仍可工作。 重点在于:这个警告不阻碍接口使用,无需因此重构现有接口体系 。很多开发者看到警告就慌,其实只要 paths 配置正确,一切照常。

5. 从接口到工程化:如何让接口真正成为团队协作的基石

5.1 接口文件组织规范:避免“types/index.ts”变成类型垃圾场

我见过最混乱的项目, types/index.ts 有2000行,全是 interface A , interface B , type C ,没有分类、没有注释、没有版本标记。新人想找个 User 接口,得Ctrl+F搜5分钟。正确的组织方式是:

  • 按领域分包 types/user/ , types/product/ , types/api/
  • 按用途分层 types/api/response/ , types/api/request/ , types/store/state/
  • 接口文件命名即业务含义 UserApiResponse.ts , ProductListRequest.ts , CartState.ts
  • 每个文件顶部加JSDoc注释 :说明该接口用途、数据来源、最后更新时间。

例如 types/api/response/UserApiResponse.ts

/**
 * 用户API响应接口(来源:/api/v1/users/{id} GET)
 * 更新时间:2024-06-15(后端v2.3.0)
 * 字段说明:
 * - user_id:数据库主键,数字类型
 * - user_name:用户昵称,最大长度20字符
 * - avatar_url:头像CDN地址,可能为空字符串
 */
export interface UserApiResponse {
  user_id: number;
  user_name: string;
  avatar_url: string;
}

这样,当后端调整字段时,只需改这个文件,所有引用处自动报错,且注释明确了变更影响范围。

5.2 接口变更的协作流程:如何让“改一个接口,不崩整个项目”

接口变更最怕“悄悄改,然后所有人报错”。我们推行的流程是:

  1. 变更提案 :在GitLab/GitHub创建Issue,标题如“【BREAKING】User API响应增加user_status字段”,描述变更内容、影响范围、迁移方案;
  2. 生成变更报告 :用 ts-morph 脚本扫描所有引用 UserApiResponse 的地方,生成影响列表;
  3. PR中强制检查 :CI流程加入 npm run type-check ,确保新接口定义通过,且所有引用处编译通过;
  4. 向后兼容 :新增字段标记 ? ,旧字段不删除,给下游1个迭代周期过渡。

我的教训:曾因急着上线,直接在 User 接口里删掉 old_field ,结果arco design的表格组件因依赖该字段崩溃。现在所有接口变更,第一件事是查 git grep "User" ,确认所有消费方。

5.3 类型即文档:用接口生成API文档与Mock数据

接口定义不仅是代码约束,更是活文档。我们用 typedoc 工具自动生成类型文档:

npm install -D typedoc
npx typedoc --out docs/types --includeDeclarations --excludePrivate src/types/

生成的HTML文档里,每个接口都有字段说明、类型、是否可选,比手写文档更准。更进一步,用 json-schema-faker 根据接口生成Mock数据:

// user.schema.ts
import { generate } from 'json-schema-faker';
import { UserApiResponse } from '@/types/api/response/UserApiResponse';

// 将TypeScript接口转为JSON Schema(需额外工具如ts-json-schema-generator)
const schema = {
  type: 'object',
  properties: {
    user_id: { type: 'number' },
    user_name: { type: 'string' },
    avatar_url: { type: 'string' }
  }
};

const mockUser = generate(schema); // { user_id: 123, user_name: 'xxx', avatar_url: 'xxx' }

这样,前端在后端接口未完成时,就能基于接口定义生成真实数据,保证开发并行。

5.4 面试中的接口考察:不止于语法,更看工程思维

最近面试了17个TypeScript候选人,问“接口和type的区别”,90%能答出“接口可合并、可实现”,但只有3人能结合项目谈“为什么在XX场景选接口”。真正的考察点是:

  • 能否识别类型漂移风险 :给你一段 any 代码,让你改用接口,并说明改哪些地方;
  • 能否设计可演进的接口 :设计一个支持分页、排序、过滤的通用列表接口;
  • 能否处理第三方类型 axios 返回值类型如何与自定义接口对接。

我的建议:面试前,把你项目里最复杂的接口体系画出来(不用代码,用文字描述: User 接口被哪些模块用、如何扩展、如何变更),这比背10条语法有用得多。

我在实际项目中发现,接口用得好的团队,Bug率低40%,Code Review时间减少60%。因为它把“人脑记住的规则”变成了“机器强制的契约”。当你下次写 function handleUser(user: any) 时,停下来想两秒:这个 user 到底长什么样?把它写成接口,不是多此一举,而是给未来的自己,留了一盏不会熄灭的灯。

更多推荐