Skip to content

复合类型与高级类型

核心问题解析

问题一:如何描述一个复杂的对象结构?

在真实项目中,数据往往很复杂:

ts
const user = {
id: 1,
profile: {
name: "Alice",
contacts: [
{ type: "email", value: "a@ex.com" },
{ type: "phone", value: "123-456" }
]
},
roles: ["admin", "user"]
};

我们需要一种方式精确描述这种嵌套结构。

答案:使用 interface 或 type 构建复合类型:

ts
interface Contact {
type: "email" | "phone";
value: string;
}

interface UserProfile {
name: string;
contacts: Contact[];
}

interface User {
id: number;
profile: UserProfile;
roles: string[];
}

现在 user 的类型就是 User,编辑器能提供完整提示和检查。

问题二:怎么让类型“动态”变化?

我们希望类型能根据输入自动适配,比如:

ts
function getProperty(obj, key) {
return obj[key];
}

理想情况下:

  • getProperty(user, 'id') → 返回 number

  • getProperty(user, 'profile') → 返回 UserProfile

  • 但 JS 参数是 any,无法保证类型安全。

答案:使用 keyof 和泛型实现动态类型映射:

ts

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

现在调用时,返回值类型会自动推导为对应属性的类型。

这就是“类型编程”的起点:让类型随输入变化。

问题三:联合类型和交叉类型有什么区别?

类型符号含义示例
联合类型(Union)“或”关系,满足其一即可 string | number
交叉类型(Intersection)&“且”关系,必须同时满足 A & B
ts
// 联合:可以是字符串或数字
let value: string | number = "hello";
value = 100; // ✅ OK

// 交叉:必须同时具备 A 和 B 的所有属性
interface A { x: number }
interface B { y: string }
type C = A & B;

const c: C = { x: 1, y: "hi" }; // ✅ 必须都有

关键区别:

  • 联合类型是“收窄”——运行时只能是其中一种
  • 交叉类型是“合并”——必须包含所有成员

学习目标详解

目标一:掌握 interface 与 type 的异同

特性interfacetype
是否可重复定义✅ 可合并(声明合并)❌ 不可重复
是否支持继承✅ extends ✅通过交叉类型模拟
是否支持计算属性✅(如 Record<K,T>
是否支持原始类型别名✅(如 type ID = string
性能更优(TS 内部优化)稍差

使用建议:

  • 对象结构优先用 interface
  • 工具类型、联合/交叉、映射类型用 type
  • 第三方库扩展用 interface(支持声明合并)
ts
interface Person {
name: string;
}

interface Person {
age: number; // 自动合并
}
type ID = string | number; // type 更适合

目标二:使用联合、交叉、元组、可选、只读

1. 联合类型 |

ts
type Status = "loading" | "success" | "error";
type ID = string | number;

配合 typeof 守卫使用:

ts
if (typeof id === "string") { ... } // 类型收窄

2. 交叉类型 &

ts
type Timestamped = { updatedAt: Date };
type Entity = { id: number } & Timestamped;

3 . 元组(Tuple)

固定长度和类型的数组:

ts 编辑 let nameAge: [string, number] = ["Bob", 25];

nameAge[0].toUpperCase(); // ✅ string 方法

4. 可选属性 ?

ts
interface User {
id: number;
nickname?: string; // 可选
}

5. 只读属性 readonly

ts
interface Config {
readonly apiUrl: string;
}

防止意外修改:

ts
config.apiUrl = "xxx"; // ❌ 报错

目标三:理解索引类型与映射类型

索引类型(Index Signatures)

描述“任意键”的对象:

ts
interface Dictionary {
[key: string]: string;
}

const dict: Dictionary = { a: "apple", b: "banana" };

也可用于数字或 symbol:

ts
interface NumberMap {
[key: number]: boolean; // 数字键 → 布尔值
}

映射类型(Mapped Types)

基于旧类型生成新类型:

ts
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};

type Optional<T> = {
[K in keyof T]?: T[K];
};

内置映射类型:

  • Partial<T>:所有属性可选
  • Readonly<T>:所有属性只读
  • Pick<T, K>:提取部分属性
  • Omit<T, K>:排除部分属性
ts
type UserPreview = Pick<User, 'id' | 'name'>;

目标四:掌握 keyof、typeof、ReturnType 等内置操作符

操作符作用示例
keyof获取类型的所有键keyof User → "id"
typeof获取值的类型typeof user → User
ReturnType<F>获取函数返回值类型ReturnType<() => string> → string
Parameters<F>获取函数参数类型Parameters<(a: number) => void> → [number]
InstanceType<C>获取类实例类型InstanceType<typeof MyClass>

实战示例:安全的属性访问

ts

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

getProp(user, 'id');     // ✅ 返回 number
getProp(user, 'email');  // ❌ 编译报错!

这比 obj['email'] 安全得多。

总结:本节核心要点

概念关键点
interface vs type接口可合并,类型更灵活
联合类型 “或”,用于多态输入
交叉类型 &“且”,用于 mixin/扩展
映射类型自动生成类型,提升复用性
keyof / typeof实现类型级“反射”

练习题

练习题 1:定义嵌套接口

为以下对象定义完整的类型结构:

ts
const blogPost = {
id: 1,
title: "Hello TS",
author: {
name: "Alice",
email: "a@ex.com"
},
tags: ["ts", "tutorial"],
published: true
};

要求:

  • 使用 interface
  • published 是可选的
  • tags 至少有一个元素(提示:可用元组 + 联合)

练习题 2:联合 vs 交叉

分析以下类型的结果:

ts
interface A { x: number; z: string }
interface B { y: number; z: number }

type Union = A | B;
type Intersection = A & B;

// 写出 Union 和 Intersection 的实际结构
// 并说明 z 的类型分别是什么

练习题 3:使用映射类型

实现一个类型 MakeOptional,将 User 的 email 和 age 属性变为可选。

ts
interface User {
id: number;
name: string;
email: string;
age: number;
}

type PartialUser = MakeOptional<User, 'email' | 'age'>;

练习题 4:索引签名限制

以下代码是否有问题?如何修正?

ts
interface Styles {
[key: string]: string;
}

const styles: Styles = {
color: "red",
fontSize: 16 // ❌ 报错?
};

练习题 5:keyof 实战

实现一个函数 updateEntity,它接受一个对象和一个键值对,安全地更新属性:

ts
function updateEntity<T, K extends keyof T>(
entity: T,
key: K,
value: T[K]
): T {
return { ...entity, [key]: value };
}

// 测试
const user = { id: 1, name: "Bob" };
updateEntity(user, "name", "Alice");     // ✅ OK
updateEntity(user, "age", 30);           // ❌ 应该报错

验证是否类型安全。