02-接口与类型别名
接口与类型别名
概述
对于 Go 开发者来说,interface 是行为抽象的代名词 —— 定义一组方法签名,任何实现了这些方法的类型都隐式满足该接口。而 TypeScript 的 interface 则完全不同:它主要用于描述对象的结构(shape),而非行为。此外,TypeScript 提供了 type 关键字用于定义类型别名,这在 Go 中没有直接对应。
Go 开发者已知
// Go 接口 —— 行为抽象
type Writer interface {
Write([]byte) (int, error)
}
// 隐式实现
type File struct{}
func (f *File) Write(p []byte) (int, error) {
return len(p), nil
}
// 接口嵌入
type ReadWriter interface {
Reader
Writer
}
// 空接口
type Any interface{}Go 接口的核心特点:
- 接口定义方法集(行为契约)
- 实现是隐式的(duck typing)
- 接口嵌入实现方法的组合
- 没有属性/字段描述能力
TypeScript 怎么做
interface 定义对象形状
// 描述对象的结构(包含字段)
interface User {
id: number
name: string
email: string
age?: number // 可选属性
readonly createdAt: Date // 只读属性
}
// 使用
const user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
createdAt: new Date(),
}
// user.createdAt = new Date() // Error: 只读方法签名
// interface 也可以包含方法
interface HttpHandler {
(req: Request): Response // 调用签名
path: string // 属性
}
interface CRUD<T> {
create(item: T): void
read(id: string): T | null
update(id: string, item: T): void
delete(id: string): void
}type 别名
// 联合类型(interface 无法表达)
type Status = 'active' | 'inactive' | 'pending'
// 交叉类型
type Named = { name: string }
type Aged = { age: number }
type Person = Named & Aged // { name: string; age: number }
// 元组类型
type Pair = [string, number]
const pair: Pair = ['Alice', 30]
// 基本类型别名
type ID = string | number
type Callback = (err: Error | null, result?: unknown) => voidextends 继承
interface Base {
id: number
}
interface Admin extends Base {
role: 'admin'
permissions: string[]
}
// Admin 继承 id 字段
// 多个继承
interface Timestamp {
createdAt: Date
updatedAt: Date
}
interface SuperAdmin extends Admin, Timestamp {
level: number
}差异分析
| 维度 | Go | TypeScript |
|---|---|---|
| 接口本质 | 方法集,行为抽象 | 对象形状/结构描述 |
| 实现方式 | 隐式(duck typing) | 显式 implements |
| 组合机制 | 接口嵌入 | extends 继承 |
| 联合类型 | 不支持 | type A | B |
| 交叉类型 | 不支持 | type A & B |
| 元组 | 数组 | type Pair = [string, number] |
| 索引签名 | 不支持 | [key: string]: unknown |
Go 的 interface{} 对应什么?
Go 的 interface{} 在 TypeScript 中最精准的映射是 unknown(安全版本)或 any(逃逸版本)。
// Go: var x interface{}
let x: unknown
x = 42
x = "string"
x = { foo: 1 }但请注意:Go 的 interface{} 在运行时保留类型信息(通过反射),而 TypeScript 的类型在运行时被抹除,这是本质区别。
Bad Practice
// Bad: 同一概念混用 interface 和 type
interface User {
name: string
}
type User = { // Error: 重复声明
age: number
}
// Bad: 用 interface 表达联合类型(做不到)
// interface Status = 'a' | 'b' | 'c' // 语法错误
// Bad: 用 type 定义可扩展的公共 API
type Point = {
x: number
y: number
}
// type 不支持声明合并,消费者无法扩展
// Bad: 索引签名过度宽泛
interface Config {
[key: string]: string // 所有属性都必须是 string
port: number // Error: number 不兼容 string
}Best Practice
// Good: 公共 API 优先用 interface(支持声明合并)
interface User {
name: string
}
interface User {
age?: number // 声明合并:扩展 User
}
// Good: 联合/交叉类型用 type
type Status = 'active' | 'inactive'
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string }
// Good: 精确的索引签名
interface SafeConfig {
port: number
host: string
[key: string]: unknown // 允许额外属性,但需收窄
}
// Good: 使用 interface 定义 Props/State 契约
interface ButtonProps {
label: string
variant?: 'primary' | 'secondary'
disabled?: boolean
onClick: () => void
}
// Good: 使用 type 定义复杂工具类型
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}声明合并(Declaration Merging)
interface 独有的声明合并特性非常强大:
// 在不同文件中逐步扩展同一个接口
// user.ts
interface User {
name: string
}
// user-extensions.ts
interface User {
age: number
}
// 最终 User 同时包含 name 和 age
const u: User = { name: 'Alice', age: 30 }type 不支持声明合并,这是选择 interface 还是 type 的关键区别。
code-tabs 对比
总结
| 场景 | 推荐构造 |
|---|---|
| 公共 API / 库类型 | interface |
| Props / State 定义 | interface |
| 需要声明合并 | interface |
| 联合 / 交叉类型 | type |
| 元组 | type |
| 工具类型 / 映射类型 | type |
| 基本类型别名 | type |
作为 Go 开发者,理解 TS interface = 结构描述(而非 Go 的行为抽象)是适应 TypeScript 的关键一步。下一章将介绍 函数签名与重载。
附录:常见问题(QA)
Q1:什么是索引签名?
索引签名(Index Signature)允许你定义一个接口/类型可以接受任意数量、符合某种模式的属性。语法是 [key: KeyType]: ValueType。
// 最基本的索引签名
interface StringMap {
[key: string]: string // 任意字符串键,值必须是 string
}
const config: StringMap = {
host: 'localhost',
port: '8080', // OK
// debug: true // Error: boolean 不兼容 string
}常见的索引签名模式:
// 1. 字符串索引 —— 最常用
interface Dictionary<T> {
[key: string]: T | undefined
}
const dict: Dictionary<number> = { a: 1, b: 2 }
// 2. 数字索引 —— 类似数组
interface NumericIndex {
[index: number]: string
}
const arr: NumericIndex = ['a', 'b', 'c']
// 3. 同时使用两种索引(数字索引的值必须兼容字符串索引的值)
interface Both {
[index: number]: string // 数字索引
[key: string]: string // 字符串索引
}
// 4. 结合已知属性
interface Config {
port: number // 已知属性
[key: string]: unknown // 额外属性
}
const c: Config = { port: 8080, host: 'local', debug: true }索引签名的类型约束
如果索引签名的值类型不是 unknown 或 any,那么所有显式声明的属性的类型也必须兼容索引签名的值类型:
// Bad: port: number 不兼容 [key: string]: string
interface Bad {
[key: string]: string
port: number // Error: Property 'port' of type 'number' is not assignable to 'string' index type
}
// Good: 使用联合类型
interface Good {
[key: string]: string | number
port: number // OK
}
// 或者用 unknown 兜底
interface Better {
port: number
[key: string]: unknown // OK,unknown 兼容所有类型
}索引签名与 Go 的对比
Go 的 map[string]interface{} 和 TS 的 { [key: string]: unknown } 在概念上类似,但有两点区别:
- 类型安全级别不同:TS 索引签名可以约束值的具体类型(如
{ [key: string]: number }),而 Go 的map[string]interface{}无法约束值类型 - TS 索引签名是编译时概念,运行时并无此约束;Go 的 map 类型是运行时真实存在的数据结构
// TS — 编译时保证值都是 number
type ScoreMap = { [key: string]: number }
const scores: ScoreMap = { alice: 95, bob: 87 }
// scores.charlie = 'A' // 编译时报错// Go — 无法约束值类型
scores := map[string]interface{}{
"alice": 95,
"bob": 87,
}
// scores["charlie"] = "A" // 运行时没问题,但取用时需要类型断言Q2:类型继承(extends)和类型合并(declaration merging)的区别是什么?
二者确实都产生了合并字段的效果,但底层机制和适用场景有本质区别。
核心区别
| 维度 | extends 继承 | 声明合并(Declaration Merging) |
|---|---|---|
| 适用类型 | interface + type(交叉) | 仅 interface |
| 产生时机 | 定义时显式声明 | 同一作用域同名 interface 自动合并 |
| 源类型变更 | 继承结果会同步反映源类型的变化 | 合并是静态快照,互不影响 |
| 覆盖行为 | 同名属性会报错(除非兼容) | 同名属性必须类型一致,否则报错 |
| 可读性 | 显式表达继承关系,易于理解 | 隐式合并,可能隐藏类型来源 |
| 跨文件 | 需要 import 源类型 | 自动合并,常用于全局类型增强 |
代码对比
// === extends 继承 ===
interface Base {
id: number
}
interface Derived extends Base {
name: string
}
// Derived = { id: number; name: string }
// 如果 Base 后续增加字段
interface Base {
version: string // 声明合并增加字段
}
// Derived 自动同步获得 version 字段
// Result: Derived = { id: number; name: string; version: string }
// === 声明合并 ===
// 文件 a.ts
interface User {
name: string
}
// 文件 b.ts(同行或不同文件)
interface User {
age: number
}
// 自动合并后 User = { name: string; age: number }
// 但是:
interface User {
name: string
email: string // Error!age 和 email 都是新字段,并非冲突;但如果你试图重定义 name 为不同类型就会报错
}
interface User {
name: number // Error: 同名属性 name 类型冲突 (string vs number)
}更重要的区别:覆盖行为
// extends 不允许覆盖不兼容的类型
interface A {
id: string
}
interface B extends A {
// id: number // Error: 类型不兼容
}
// 声明合并要求同名属性类型完全一致
interface C {
id: string
}
interface C {
// id: number // Error: 同名属性类型冲突
}实际应用场景
声明合并最强大的用途是扩展现有类型——尤其是第三方库的类型:
// 扩展现有库的类型(无需修改源码)
// express.d.ts
import 'express'
declare module 'express' {
interface Request {
user?: { // 声明合并:给 Express Request 增加 user 属性
id: number
name: string
}
}
}
// 使用
app.get('/', (req, res) => {
console.log(req.user?.name) // OK,类型安全
})// extends — 适合创建新类型
// 更清晰、可预测,推荐在业务代码中使用
interface BaseEntity {
id: number
createdAt: Date
}
interface Product extends BaseEntity {
name: string
price: number
}何时用哪个?
- 声明合并:用于扩展现有类型(第三方库、全局类型)、分文件定义同一模型
- extends 继承:用于创建新类型,表达"B 是 A 的扩展"的语义关系,推荐日常开发首选