接口与类型别名:interface定义对象结构、type定义联合类型、接口继承、类型合并

好,咱们今天聊点实在的。TypeScript 里最常用的两个工具——interfacetype,到底怎么选?怎么用?我见过不少项目,代码里混用一通,最后维护起来那叫一个头疼。今天我把我的经验掰开揉碎讲给你听。

interface:定义对象结构的首选

我个人习惯,只要定义对象的结构,优先用 interface。为什么?因为它更直观,而且 Vue 的生态对它支持更好。

// 定义一个用户对象
interface User {
  id: number
  name: string
  email: string
  age?: number  // 可选属性
  readonly createdAt: Date  // 只读属性
}

const user: User = {
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  createdAt: new Date()
}

这里有个小细节——readonly。我在项目中遇到过,有人把 createdAt 写成普通属性,结果后面不小心改了它,导致数据不一致。嗯,这种坑踩过一次就记住了。

我的建议: 对于 Vue 组件里的 props、data、computed 返回值,优先用 interface 定义。这样 IDE 的智能提示最友好。

type:联合类型和交叉类型的利器

type 什么时候用?说白了,当你需要定义联合类型、交叉类型,或者一些复杂的类型组合时,type 才是主角。

// 联合类型:一个值可以是多种类型之一
type Status = 'active' | 'inactive' | 'pending'
type ID = string | number

// 交叉类型:合并多个类型
type AdminUser = User & { role: 'admin'; permissions: string[] }

// 类型别名也可以定义对象,但我不推荐
type Product = {
  id: number
  name: string
  price: number
}

你想想看,如果不用联合类型,你可能会写一堆 if-else 判断,还容易漏掉某个分支。我曾经在一个支付系统里,用联合类型定义了订单状态,编译时就能发现所有未处理的状态分支——这种安全感,写代码的人最懂。

注意: type 定义的对象结构,在 Vue 的 props 类型推导中偶尔会有小问题。我建议对象结构用 interface,联合类型用 type,各司其职。

接口继承:复用与扩展

接口继承是 TypeScript 里非常实用的特性。它让你能基于已有接口扩展新接口,避免重复定义。

interface BaseEntity {
  id: number
  createdAt: Date
  updatedAt: Date
}

interface User extends BaseEntity {
  name: string
  email: string
}

interface Post extends BaseEntity {
  title: string
  content: string
  author: User
}

// 也可以继承多个接口
interface Comment extends BaseEntity, HasAuthor {
  text: string
}

我在重构一个后台管理系统时,发现所有数据模型都有 idcreatedAtupdatedAt 这三个字段。一开始每个接口都写一遍,后来抽成 BaseEntity,代码量直接减半。嗯,这就是继承的魅力。

核心原则: 把公共字段抽到基接口,用继承来扩展。这样改一个地方,所有子接口都同步更新。

类型合并:interface 的独特能力

这一点很多人不知道——interface 可以声明合并,而 type 不行。什么意思?就是同一个名字的 interface 可以定义多次,TypeScript 会自动合并它们。

// 第一次定义
interface User {
  name: string
}

// 第二次定义,自动合并
interface User {
  age: number
}

// 最终 User 包含 name 和 age
const user: User = {
  name: '李四',
  age: 25
}

这个特性在扩展第三方库的类型时特别有用。比如你想给 Vue 的 ComponentCustomProperties 添加自定义属性,就可以用声明合并。

// 在 .d.ts 文件中
import 'vue'

declare module 'vue' {
  interface ComponentCustomProperties {
    $api: typeof api
    $t: typeof translate
  }
}

我曾经在一个大型项目中,需要给 Vue 实例挂载全局方法。如果没有声明合并,就得用 any 绕过类型检查——那滋味,谁用谁知道。

注意: type 不支持声明合并。如果你用 type 定义对象,后面想扩展就只能用交叉类型(&),而且不能重复定义同名 type。

如何选择:interface vs type

我总结了一套选择规则,你可以直接拿来用:

场景 推荐用 原因
定义对象结构 interface 更直观,支持声明合并,Vue 生态友好
定义联合类型 type type 天然支持联合类型
定义交叉类型 type type 用 & 符号更简洁
需要声明合并 interface type 不支持重复定义
定义函数类型 type type 定义函数签名更简洁
定义元组类型 type type 支持元组语法
我的经验: 在 Vue 项目中,我 80% 的场景用 interface,20% 用 type。interface 负责对象结构,type 负责联合类型和工具类型。这个比例你可以参考。

实战:在 Vue 组件中应用

来看一个完整的例子,把今天讲的知识点串起来:

// 1. 定义基础类型
type Status = 'draft' | 'published' | 'archived'

// 2. 定义对象接口
interface Article {
  id: number
  title: string
  content: string
  status: Status
  tags: string[]
  publishedAt?: Date
}

// 3. 接口继承
interface FeaturedArticle extends Article {
  coverImage: string
  summary: string
}

// 4. 在组件中使用
import { defineComponent, PropType } from 'vue'

export default defineComponent({
  props: {
    article: {
      type: Object as PropType<Article>,
      required: true
    },
    status: {
      type: String as PropType<Status>,
      default: 'draft'
    }
  },
  setup(props) {
    // 这里 props.article 有完整的类型提示
    // props.status 只能是 'draft' | 'published' | 'archived'
    return {
      displayTitle: computed(() => {
        if (props.article.status === 'published') {
          return props.article.title
        }
        return `[草稿] ${props.article.title}`
      })
    }
  }
})

你看,这样写下来,整个组件的类型是自文档化的。新同事接手代码,看一眼类型定义就知道数据长什么样,能做什么操作。我曾经在一个没有类型定义的项目里,为了搞清楚一个对象的结构,翻了五个文件——那种痛苦,经历过一次就再也不想经历了。

总结一下: interface 和 type 不是二选一的关系,而是互补的。用 interface 定义对象结构,用 type 定义联合类型和工具类型。记住这个原则,你的 TypeScript 代码会清晰很多。

好了,这一章的内容就到这里。下一章我们聊聊泛型——那个让 TypeScript 真正强大的特性。到时候见。