对象与接口:数据的形状与契约

目录

第 1 节 概述

前几篇我们学了基础类型和数组。这就像学会了描述单个数字和列表。

但程序的数据往往是结构化的——一个"人"有姓名、年龄、地址;一个"订单"有编号、商品、金额。

TypeScript 提供了两种方式描述对象的结构:interfacetype

第 2 节 对象类型

最直接的方式是用 type 描述对象的形状:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Person = {
  name: string
  age: number
  email: string
}

const user: Person = {
  name: "张三",
  age: 18,
  email: "zhangsan@example.com",
}

2.1 可选属性

? 标记属性可选:

1
2
3
4
5
6
7
type Person = {
  name: string
  age?: number       // 年龄不是必填的
}

const user1: Person = { name: "张三", age: 18 }  // ✅
const user2: Person = { name: "李四" }             // ✅ age 可选

2.2 只读属性

readonly 标记属性不可修改:

1
2
3
4
5
6
7
8
type Config = {
  readonly url: string
  timeout: number
}

const config: Config = { url: "https://api.com", timeout: 5000 }
config.url = "https://new.com"   // ❌ 只读,不能修改
config.timeout = 3000             // ✅ 非只读,可以改

第 3 节 interface

interface 是描述对象类型的主要方式。和 type 很像,但有一些独特的能力:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface Person {
  name: string
  age: number
  email: string
}

const user: Person = {
  name: "张三",
  age: 18,
  email: "zhangsan@example.com",
}

3.1 声明合并

同名的 interface 会自动合并:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
interface Person {
  name: string
}

interface Person {
  age: number
}

// 等价于
interface Person {
  name: string
  age: number
}

这个特性对扩展第三方库的类型很有用。type 不支持声明合并,重名会报错。

3.2 继承

interface 可以用 extends 继承另一个接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface Animal {
  name: string
  eat(): void
}

interface Dog extends Animal {
  breed: string       // 品种
}

const myDog: Dog = {
  name: "旺财",
  breed: "金毛",
  eat() {
    console.log("旺财在吃饭")
  },
}

可以同时继承多个接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface Flyable {
  fly(): void
}

interface Swimmable {
  swim(): void
}

interface Duck extends Flyable, Swimmable {
  quack(): void
}

第 4 节 type vs interface

两者大部分功能重叠,但有几处关键区别:

对比typeinterface
声明合并❌ 不支持✅ 同名自动合并
继承& 交叉类型extends 继承
联合类型type A = B | C❌ 不支持
基本类型别名type Name = string❌ 不支持
类实现✅ 支持 implements✅ 支持 implements

什么时候用哪个?

一个实用的准则是:

  • 需要描述对象/类的形状→ 用 interface
  • 需要联合类型、交叉类型、基本类型别名→ 用 type
  • 公开 API / 库的类型定义→ 用 interface(方便消费者扩展)
  • 内部工具类型、组件 props→ 都可以,保持一致即可
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// interface 适用于:对象结构、类的契约
interface User {
  id: number
  name: string
}

// type 适用于:联合类型、工具类型
type Status = "active" | "inactive" | "banned"
type Nullable<T> = T | null
type ApiResponse<T> = {
  data: T
  status: Status
}

第 5 节 接口描述函数

interface 也可以描述函数的形状——调用签名

1
2
3
4
5
6
7
interface SearchFunc {
  (source: string, subString: string): boolean
}

const mySearch: SearchFunc = function(source, subString) {
  return source.includes(subString)
}

还有带属性的函数——既是函数,又有附加属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface Counter {
  (start: number): string
  interval: number
  reset(): void
}

function getCounter(): Counter {
  const counter = ((start: number) => "计数") as Counter
  counter.interval = 1000
  counter.reset = () => {}
  return counter
}
说明

带属性函数的场景不多见,但在设计库的 API 时会用到。比如 jQuery 的 $ 既是函数,又有 $.ajax 等方法。

第 6 节 索引签名

当对象的属性名不确定时,可以用索引签名描述:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface StringArray {
  [index: number]: string
}

const myArray: StringArray = ["a", "b", "c"]

interface Dictionary {
  [key: string]: unknown
}

const dict: Dictionary = {
  "foo": 123,
  "bar": "hello",
}

索引签名常用于"字典"或"映射"场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface UserMap {
  [userId: string]: {
    name: string
    age: number
  }
}

const users: UserMap = {
  "u001": { name: "张三", age: 18 },
  "u002": { name: "李四", age: 20 },
}

有了对象和接口,你已经能描述复杂的数据结构了。

但程序不只有数据——还有行为。 函数是行为的基本单位。下一篇看怎么给函数加上类型。

目录