一、简介
TypeScript 除了基础类型外,还提供了一系列高级类型特性,这些特性通过组合、转换和约束基础类型,解决了复杂场景下的类型定义问题。本文将详细讲解 TypeScript 高级类型的核心概念、使用场景及实战技巧,帮助开发者写出更精准、更灵活的类型标注。
二、交叉类型(Intersection Types)
交叉类型(A & B)用于将多个类型合并为一个新类型,新类型包含所有源类型的成员。可以理解为“同时满足所有类型的约束”,类似于逻辑上的“与(AND)”操作。
2.1 基础用法
// 定义两个基础类型
type User = { id: number; name: string };
type Contact = { phone: string; email: string };
// 交叉类型:同时包含 User 和 Contact 的所有属性
type UserWithContact = User & Contact;
// 必须满足所有类型的约束
const user: UserWithContact = {
id: 1,
name: "懒羊羊",
phone: "110",
email: "lanyangyang@qq.com"
};
2.2 特殊场景:交叉类型与原始类型
当交叉类型涉及原始类型时,只有“兼容的类型”才能交叉出有效结果:
// 交叉两个相同的原始类型,结果仍为该类型
type NumAndNum = number & number; // 等价于 number
// 交叉不兼容的原始类型,结果为 never(无有效值)
type StrAndNum = string & number; // 等价于 never(没有值能同时是 string 和 number)
// 交叉函数类型(合并参数和返回值约束)
type F1 = (a: number) => string;
type F2 = (b: string) => number;
type F3 = F1 & F2;
// F3 等价于 (a: number & string) => string & number → (a: never) => never
const f3: F3 = () => {
throw new Error();
};
2.3 实用场景:扩展对象类型
交叉类型常用于“给现有类型动态添加属性”,避免重复定义:
// 基础用户类型
type BaseUser = { id: number; name: string };
// 扩展类型:添加时间戳属性
type TimedUser = BaseUser & { createdAt: Date; updatedAt: Date };
// 扩展类型:添加权限属性
type AdminUser = BaseUser & { role: "admin"; permissions: string[] };
三、联合类型(Union Types)
联合类型(A | B)表示一个值可以是多个类型中的任意一种,类似于逻辑上的“或(OR)”操作。与交叉类型的“同时满足”不同,联合类型是“满足其一即可”。
3.1 基础用法
// 联合类型:值可以是 string 或 number
type StringOrNumber = string | number;
let value: StringOrNumber;
value = "hello"; // 正确
value = 123; // 正确
// value = true; // 错误:boolean 不在联合类型中
3.2 类型收窄(Type Narrowing)
使用联合类型时,需通过类型判断(如 typeof、instanceof、in 等)“收窄”类型范围,才能安全访问特定类型的属性:
type User = { type: "user"; name: string };
type Admin = { type: "admin"; role: string };
type Person = User | Admin;
function logPerson(person: Person) {
// 错误:联合类型中并非所有类型都有 name 属性
// console.log(person.name);
// 方式1:通过字面量类型收窄(最常用)
if (person.type === "user") {
// 此时 TypeScript 自动推断为 User 类型
console.log("用户:", person.name);
} else {
// 此时自动推断为 Admin 类型
console.log("管理员:", person.role);
}
// 方式2:通过 in 运算符检查属性
if ("name" in person) {
console.log(person.name);
} else {
console.log(person.role);
}
}
3.3 可区分联合(Discriminated Unions)
可区分联合是一种特殊的联合类型,通过一个“公共属性”(通常称为 tag)来区分不同类型,是处理多状态场景的最佳实践:
// 定义三种状态类型,均包含 type 作为区分标识
type LoadingState = { type: "loading" };
type SuccessState = { type: "success"; data: string };
type ErrorState = { type: "error"; message: string };
// 可区分联合:三种状态的集合
type RequestState = LoadingState | SuccessState | ErrorState;
// 根据 type 自动收窄类型
function handleState(state: RequestState) {
switch (state.type) {
case "loading":
console.log("加载中...");
break;
case "success":
// 自动推断为 SuccessState,可安全访问 data
console.log("成功:", state.data.toUpperCase());
break;
case "error":
// 自动推断为 ErrorState,可安全访问 message
console.log("失败:", state.message);
break;
}
}
四、类型别名(Type Aliases)
类型别名(type)用于给现有类型起一个新名字,类似于变量的命名,但作用于类型。它可以简化复杂类型的引用,也能定义无法用接口(interface)表达的类型(如联合类型、交叉类型)。
4.1 基础用法
// 给基础类型起别名
type Age = number;
let userAge: Age = 25; // 等价于 number
// 给联合类型起别名
type ID = string | number;
let userId: ID = "123"; // 正确
userId = 456; // 正确
// 给对象类型起别名
type Point = { x: number; y: number };
const p: Point = { x: 10, y: 20 };
4.2 与接口(Interface)的区别
类型别名和接口都可用于定义对象类型,但存在关键差异:
// 类型别名:不可重复定义,重复会报错
type User = { name: string };
// type User = { age: number }; // 错误:标识符“User”重复
// 接口:可重复定义,自动合并
interface User { name: string }
interface User { age: number } // 正确:合并为 { name: string; age: number }
// 类型别名支持联合/交叉类型,接口不支持
type A = { a: number } | { b: string }; // 正确
// interface A = { a: number } | { b: string }; // 错误
选择原则:
- 若需“合并类型”或定义“类的继承结构”,用
interface; - 若需定义联合类型、交叉类型或给基础类型起别名,用
type。
五、索引类型(Index Types)
索引类型用于通过“索引”动态访问对象的属性类型,结合 keyof 操作符和索引访问类型(T[K]),可实现对对象属性的类型安全操作。
5.1 keyof 操作符
keyof T 用于获取类型 T 的所有属性名组成的联合类型,相当于“属性名的元数据”:
type User = { id: number; name: string; age: number };
// keyof User 等价于 "id" | "name" | "age"
type UserKeys = keyof User;
let key: UserKeys;
key = "id"; // 正确
key = "name"; // 正确
// key = "email"; // 错误:"email" 不是 User 的属性
5.2 索引访问类型(T[K])
T[K] 用于获取类型 T 中属性 K 的类型,K 可以是单个属性名或联合类型:
type User = { id: number; name: string; age: number };
// 获取单个属性的类型:User["id"] 等价于 number
type IDType = User["id"];
// 获取多个属性的类型:联合类型
type NameOrAge = User["name" | "age"]; // 等价于 string | number
// 结合 keyof 获取所有属性的类型联合
type UserValueTypes = User[keyof User]; // 等价于 number | string
5.3 实战场景:类型安全的属性访问函数
利用索引类型可实现“获取对象任意属性值”的函数,同时保证类型安全:
// 函数:获取对象 obj 中属性 key 的值
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "张三", age: 20 };
// 正确:key 必须是 user 的属性,返回值类型自动推断
const id = getProperty(user, "id"); // id: number
const userName = getProperty(user, "name"); // name: string
// 错误:key 不是 user 的属性,编译时直接报错
// const email = getProperty(user, "email");
console.log(id, userName); // 1 "张三"
六、映射类型(Mapped Types)
映射类型用于“基于现有类型创建新类型”,通过遍历源类型的属性(keyof T)并修改其特性(如只读、可选)生成新类型,是 TypeScript 类型编程的核心工具。
6.1 基础语法
// 映射类型语法:遍历 T 的所有属性 K,对每个属性做转换
type MappedType<T> = {
[K in keyof T]: T[K]; // 此处为“复制源类型”,无修改
};
type User = { id: number; name: string };
type UserCopy = MappedType<User>; // 等价于 User
6.2 常用映射工具类型
TypeScript 内置了多个常用映射类型(位于 lib.es5.d.ts),掌握它们可大幅提升开发效率:
1. Readonly:将所有属性转为只读
type ReadonlyUser = Readonly<User>;
// 等价于:
// {
// readonly id: number;
// readonly name: string;
// }
const user: ReadonlyUser = { id: 1, name: "张三" };
// user.id = 2; // 错误:只读属性不可修改
2. Partial:将所有属性转为可选
type PartialUser = Partial<User>;
// 等价于:
// {
// id?: number;
// name?: string;
// }
// 适合“部分更新”场景,无需传递所有属性
function updateUser(user: User, changes: PartialUser): User {
return { ...user, ...changes };
}
3. Required:将所有属性转为必选(与 Partial 相反)
type OptionalUser = { id?: number; name?: string };
type RequiredUser = Required<OptionalUser>; // 等价于 { id: number; name: string }
4. Pick<T, K>:从 T 中选取部分属性 K 组成新类型
// 从 User 中选取 "name" 属性
type UserName = Pick<User, "name">; // 等价于 { name: string }
// 选取多个属性(联合类型)
type UserIdAndName = Pick<User, "id" | "name">; // 等价于 { id: number; name: string }
5. Omit<T, K>:从 T 中排除部分属性 K 组成新类型(与 Pick 相反)
// 从 User 中排除 "age" 属性
type UserWithoutAge = Omit<User, "age">; // 等价于 { id: number; name: string }
6.3 自定义映射类型
可根据需求自定义映射规则,例如“将所有属性类型转为字符串”:
// 自定义映射:将 T 的所有属性值类型转为 string
type Stringify<T> = {
[K in keyof T]: string;
};
type User = { id: number; age: number };
type StringifiedUser = Stringify<User>;
// 等价于 { id: string; age: string }
七、条件类型(Conditional Types)
条件类型用于“根据条件判断选择类型”,语法为 T extends U ? X : Y(若 T 是 U 的子类型,则返回 X,否则返回 Y),支持嵌套和递归,是实现复杂类型逻辑的基础。
7.1 基础用法
// 条件类型:若 T 是 string,则返回 number,否则返回 boolean
type Conditional<T> = T extends string ? number : boolean;
type A = Conditional<string>; // A = number(T 是 string)
type B = Conditional<number>; // B = boolean(T 不是 string)
7.2 分布式条件类型
当条件类型的泛型参数是联合类型时,会自动“分布式”应用到每个成员:
type ToArray<T> = T extends any ? T[] : never;
// 分布式应用:string → string[],number → number[]
type StrOrNumArray = ToArray<string | number>; // 等价于 string[] | number[]
7.3 常用内置条件类型
TypeScript 内置了多个实用条件类型:
1. Exclude<T, U>:从 T 中排除可赋值给 U 的类型
type T = "a" | "b" | "c";
type U = "a" | "d";
type Excluded = Exclude<T, U>; // 等价于 "b" | "c"(排除 T 中与 U 重叠的 "a")
2. Extract<T, U>:从 T 中提取可赋值给 U 的类型(与 Exclude 相反)
type Extracted = Extract<T, U>; // 等价于 "a"(提取 T 中与 U 重叠的 "a")
3. ReturnType:获取函数 T 的返回值类型
type Fn = (a: number) => string;
type Return = ReturnType<Fn>; // 等价于 string(Fn 的返回值类型)
// 实战:获取异步函数的返回值类型
type AsyncFn = () => Promise<{ id: number; name: string }>;
type AsyncReturn = ReturnType<AsyncFn>; // 等价于 Promise<{ id: number; name: string }>
4. Parameters:获取函数 T 的参数类型组成的元组
type Fn = (name: string, age: number) => void;
type Params = Parameters<Fn>; // 等价于 [string, number](参数类型元组)
// 实战:复用函数参数类型
function createUser(name: string, age: number) {
return { name, age };
}
// 获取 createUser 的参数类型,用于其他函数
type CreateUserParams = Parameters<typeof createUser>; // [string, number]
八、类型断言与类型守卫
类型断言和类型守卫用于在“TypeScript 无法自动推断类型”时,手动辅助类型检查,确保类型操作的安全性。
8.1 类型断言(Type Assertion)
类型断言用于“告诉编译器:我知道这个值的类型比你推断的更具体”,语法为 value as Type 或 <Type>value(推荐前者,避免与 JSX 冲突)。
// 场景1:从联合类型中指定更具体的类型
type StringOrNumber = string | number;
let value: StringOrNumber = "hello";
let strLength = (value as string).length; // 断言为 string,安全访问 length
// 场景2:断言为更具体的对象类型
type APIResponse = { data: unknown };
const response: APIResponse = { data: { id: 1, name: "张三" } };
// 断言 data 为具体类型(需确保实际结构匹配,否则运行时可能出错)
const user = response.data as { id: number; name: string };
console.log(user.name);
// 非空断言
const fixed = (name: string | null): string => {
const postfix = (epithet: string) => {
// 确保 name 不为 null 或 undefined
return name!.charAt(0) + '. the ' + epithet; // ok
}
return postfix("great");
}
console.log(fixed("lan")); // l. the great
注意:类型断言是“编译时”的类型调整,不会影响运行时类型,若断言错误(如将 number 断言为 string),运行时可能抛出错误。
8.2 类型守卫(Type Guard)
类型守卫是返回值为布尔值的函数,通过 parameter is Type 语法告诉编译器:当函数返回 true 时,parameter 的类型为 Type,常用于复杂类型收窄。
// 定义类型守卫:判断 value 是否为 User 类型
type User = { id: number; name: string };
function isUser(value: unknown): value is User {
// 检查 value 是对象,且包含 id(number)和 name(string)属性
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof (value as User).id === "number" &&
"name" in value &&
typeof (value as User).name === "string"
);
}
// 使用类型守卫收窄类型
function handleValue(value: unknown) {
if (isUser(value)) {
// 此时 value 自动推断为 User 类型
console.log(value.name);
} else {
console.log("不是 User 类型");
}
}
常见的内置类型守卫包括 typeof(检查原始类型)、instanceof(检查类实例)、Array.isArray(检查数组)等。
九、总结
TypeScript 高级类型是构建复杂类型系统的核心,其设计目标是“在保证类型安全的同时,保留 JavaScript 的灵活性”。本文介绍的交叉类型、联合类型、索引类型、映射类型、条件类型等特性,可通过组合实现强大的类型逻辑:
- 交叉类型用于合并多个类型的约束;
- 联合类型结合“类型收窄”可处理多状态场景;
- 索引类型和映射类型用于动态操作对象属性的类型;
- 条件类型支持基于条件选择类型,是类型编程的基础;
- 类型断言和类型守卫用于辅助编译器处理复杂类型推断。
在实际开发中,高级类型的核心价值在于“用类型描述业务逻辑”,例如通过可区分联合描述状态流转、通过映射类型复用对象结构、通过条件类型提取函数参数等。掌握这些特性,不仅能提升代码的健壮性,还能让类型定义成为“业务文档”的一部分,降低团队协作成本。

299

被折叠的 条评论
为什么被折叠?



