复合类型与高级类型
核心问题解析
问题一:如何描述一个复杂的对象结构?
在真实项目中,数据往往很复杂:
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 的异同
| 特性 | interface | type |
|---|---|---|
| 是否可重复定义 | ✅ 可合并(声明合并) | ❌ 不可重复 |
| 是否支持继承 | ✅ 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); // ❌ 应该报错验证是否类型安全。