TypeScript 条件类型中的 never 在过滤联合类型中的应用
TypeScript 的条件类型配合 never 类型,提供了一种极其强大的“过滤”机制。利用这一特性,可以从联合类型中精确剔除不需要的成员,或者从对象类型中提取特定属性的键。本文将详细介绍如何利用 Distributive Conditional Types(分布式条件类型)和 never 实现类型过滤。
核心原理:never 的吸收性
在 TypeScript 中,never 代表“不存在的类型”。在联合类型中,never 具有特殊的吸收性质:任何类型与 never 组成的联合类型,其结果都是该类型本身。
公式如下:
$$ A \mid \text{never} = A $$
这意味着,如果我们能让条件类型在需要剔除成员时返回 never,而在需要保留成员时返回该成员本身,最终的联合类型就会自动过滤掉 never。
基础应用:从联合类型中剔除指定类型
通过分布式条件类型,我们可以构建一个基础的过滤工具。当泛型 T 是一个联合类型时,TypeScript 会将条件类型分发到联合类型的每一个成员上。
1. 定义基础过滤类型
创建一个名为 MyExclude 的类型别名,用于从 T 中剔除 U。
type MyExclude<T, U> = T extends U ? never : T;
2. 验证过滤效果
定义一个包含多种类型的联合变量,并应用 MyExclude。
type AllTypes = string | number | boolean | string[];
// 移除 string 类型
type Result = MyExclude<AllTypes, string>;
// 等同于:type Result = number | boolean | string[];
分析其执行过程:
T为string | number | boolean | string[]。- TypeScript 分发检查每个成员:
string extends string? 成立 -> 返回never。number extends string? 不成立 -> 返回number。boolean extends string? 不成立 -> 返回boolean。string[] extends string? 不成立 -> 返回string[]。
- 最终结果组合:
never | number | boolean | string[]。 - 根据吸收性,结果简化为
number | boolean | string[]。
为了更直观地理解这一过程,可以参考以下分发流程:
进阶应用:提取对象的函数属性键
在实际开发中,经常需要从一个对象中筛选出特定类型的方法名。例如,获取一个类或接口中所有函数类型的名称。
1. 定义过滤逻辑
编写一个 FunctionKeys 类型。它遍历对象 T 的所有键 K,检查 T[K] 是否为函数。如果是,保留键 K;否则,返回 never。
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
注意最后的 [keyof T]。这是一个索引访问操作,它将前面映射类型的所有值取出来组成一个联合类型。
2. 创建示例接口
声明一个包含多种属性类型的 User 接口。
interface User {
id: number;
name: string;
email: string;
login: () => boolean;
logout: () => void;
updateProfile: (data: object) => void;
}
3. 应用类型过滤
使用 FunctionKeys 提取 User 接口中的方法名。
type UserMethods = FunctionKeys<User>;
// 结果类型为: "login" | "logout" | "updateProfile"
解析内部步骤:
- 映射类型遍历
id,name,email,login,logout,updateProfile。 - 对于
id(number),number extends Function为假,值变为never。 - 对于
login(Function),Function extends Function为真,值变为"login"。 - 以此类推,生成中间对象结构:
{ id: never; name: never; email: never; login: "login"; logout: "logout"; updateProfile: "updateProfile"; } - 通过
[keyof User]取值:never | never | never | "login" | "logout" | "updateProfile"。 never被吸收,最终得到"login" | "logout" | "updateProfile"。
实战场景:过滤非空值
处理 API 响应时,通常会得到可能包含 null 或 undefined 的联合类型。我们可以利用 never 构建一个严格的非空类型。
1. 定义 NonNullable 类型
虽然 TypeScript 内置了 NonNullable,但我们可以手动实现它以理解其原理。
type MyNonNullable<T> = T extends null | undefined ? never : T;
2. 在代码中使用
假设有一个可能为空的变量类型,应用 MyNonNullable 进行清洗。
type ApiResponse = string | number | null | undefined;
type SafeResponse = MyNonNullable<ApiResponse>;
// 结果类型为: string | number
综合案例:构建类型安全的工具函数
结合上述知识,我们可以构建一个能够自动推断对象属性类型的工具函数,例如 pickByType,它只保留对象中特定类型的属性。
1. 定义返回值类型
设计一个 PickByType 类型,它接收对象 T 和类型 U,返回只包含类型为 U 的新对象类型。
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
这里使用了 Key Remapping(键重映射)功能 as。如果属性值类型匹配,键名保持不变;否则,键名变为 never。在对象字面量类型中,键名为 never 的属性会被自动移除。
2. 实现工具函数
编写 JavaScript 逻辑来配合这个类型定义。
function pickByType<T, U>(obj: T, type: U): PickByType<T, U> {
return Object.fromEntries(
Object.entries(obj).filter(([_, value]) => {
return typeof value === typeof type;
})
) as PickByType<T, U>;
}
3. 验证效果
准备一个混合数据的对象,并调用该函数。
const config = {
id: 1,
port: 8080,
host: "localhost",
debug: true,
connect: () => console.log("connecting"),
};
// 只提取数字类型的属性
const numericConfig = pickByType(config, 0);
// numericConfig 的类型被推断为:
// {
// id: number;
// port: number;
// }
// 以下代码会报错,因为 host 和 debug 已经被类型系统过滤掉了
console.log(numericConfig.host);
console.log(numericConfig.debug);
通过将条件不匹配时的键映射为 never,TypeScript 在构建最终对象类型时自动丢弃了这些键,从而实现了精确的类型收窄。

暂无评论,快来抢沙发吧!