本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
原文:《TypeScript Types That Scared Me — Until I Learned These 3 Rules》[1]
作者:Amaresh Adak[2]
当我第一次看到 TypeScript 的 infer 和条件类型时,我反手就关了浏览器标签页,心里默念:“这辈子别再让我看见你们!”。它们看起来就像黑魔法——一堆抽象的符号和尖括号扭曲在一起,感觉就是为了搞我心态而设计的。
像 T extends (infer U)[] ? U : never 或者 DistributiveConditional<T> 这种类型,一度让我怀疑人生,甚至开始琢磨要不要转行。但!是!朋友们,重点来了:这些玩意儿根本不是什么高深莫测的概念,只是披着吓人语法外衣的纸老虎罢了。
只要你掌握了三个简单的思维模型,它们就一点也不可怕了。在这篇文章里,我就带你走一遍那些让我豁然开朗的核心概念——保证全是实战干货,不玩虚的。
别怕,你不是一个人在战斗
这种恐惧,我们都懂
我得先说清楚:如果你曾被 TypeScript 的高级类型吓到过,恭喜你,你找到了组织。别说你了,就连经验丰富的老鸟,第一次碰到条件类型和 infer 关键字时,也得懵圈一会儿。
我至今还记得,当初我盯着下面这行代码,感觉自己像个迷失在代码森林里的小白兔:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;infer R 到底是个什么鬼?为啥这么多问号和冒号?这代码看起来就像是某人脸滚键盘一通乱敲,然后就提交了。
但说真的,我多希望当时有人能告诉我:这些高级类型其实都有套路可循。一旦你理解了底层的思维模型,你就会发现它们无处不在——更重要的是,你将学会如何在合适的时机、用正确的方式驾驭它们。
规则一:条件类型,就是类型世界的 if...else
“搞得定 if...else,就搞得定条件类型。”
这是我的第一个顿悟。TypeScript 里的条件类型,工作原理跟 JavaScript 里的条件语句一毛一样,只不过它是在 “类型” 层面做判断,而不是在 “值” 层面。
基本模式简单粗暴:
type MyType<T> = T extends SomeCondition ? TrueResult : FalseResult;咱们从一个简单的例子入手:
type IsString<T> = T extends string ? true : false;type A = IsString<'hello'>; // truetype B = IsString<123>; // falsetype C = IsString<boolean>; // false看到了吗?这不就是个给类型用的 if 语句嘛。当 extends 左边的类型能赋值给右边的类型时,你就得到第一个分支(“true” 分支)的类型;否则,就得到第二个分支(“false” 分支)的类型。
接下来,上点实用的。 假设你在写一个组件,它的行为需要根据传入的 props 动态改变:
type ButtonProps<T extends boolean> = { loading: T;} & (T extends true ? { onClick?: never; disabled: true } : { onClick: () => void; disabled?: boolean });// 当 loading 是 true 时,onClick 属性就得“滚蛋”,而且 disabled 必须为 trueconst loadingButton: ButtonProps<true> = { loading: true, disabled: true, // onClick: () => {} // ❌ 类型错误!loading 的时候不准点!};// 当 loading 是 false 时,onClick 就是必须的了const normalButton: ButtonProps<false> = { loading: false, onClick: () => console.log('点我呀!'), disabled: false};思维模型就是: 你可以把条件类型想象成 TypeScript 在说:“嘿,如果这个类型长得像那个,那就给我这个类型;不然的话,就给我另一个。”
规则二:裸类型参数的 “分发” 魔术
“你给它一个联合类型,TypeScript 就会自动帮你‘遍历’——除非你喊停。”
这个规则我花了更长时间才搞明白,但一旦顿悟,那感觉,简直不要太爽。
当条件类型作用于一个泛型时,如果你给这个泛型传入一个联合类型,它就会变得具有 “分发性”。这意味着 TypeScript 会自动地、分别地将条件类型应用到联合类型的每一个成员上。
关键点来了: 这种 “分发” 行为只在泛型参数是 “裸类型” 的时候发生。
// 这就是“裸类型”—— T 直接出现在 extends 子句里type ToArray<T> = T extends any ? T[] : never;type Result = ToArray<'a' | 'b' | 'c'>;// 结果是: 'a'[] | 'b'[] | 'c'[]// 而不是: ('a' | 'b' | 'c')[]为啥会这样呢? TypeScript 把联合类型 'a' | 'b' | 'c' 给 “拆开” 了,然后逐一处理:
•
'a' extends any ? 'a'[] : never→'a'[]•
'b' extends any ? 'b'[] : never→'b'[]•
'c' extends any ? 'c'[] : never→'c'[]
最后,它把所有结果再用联合类型组合起来:'a'[] | 'b'[] | 'c'[]
但是,如果你不想要这种自动分发,也有办法关掉它:
// 用方括号把 T 包起来,让它不再“裸奔”type NoDistribute<T> = [T] extends [any] ? T[] : never;type Result2 = NoDistribute<'a' | 'b' | 'c'>;// 结果就变成了: ('a' | 'b' | 'c')[]来个实战例子: 从联合类型中过滤掉某些类型。
Copytype NonNullable<T> = T extends null | undefined ? never : T;type Clean = NonNullable<string | null | number | undefined>;// Clean 的结果是: string | number// null 和 undefined 被自动过滤掉了,干净!这种分发特性可以用来过滤联合类型,这正是 TypeScript 内置的 Exclude 工具类型的工作原理。
规则三:用 infer 偷窥类型内部
“你可以像用模式匹配一样,从其他类型中提取类型。”
infer 关键字是我 TypeScript 学习之路上的终极 BOSS。但一旦我征服了它,整个世界都清晰了。
你可以把 infer 想象成这样一句话: “嘿,TypeScript,我现在还不知道这个类型具体是啥,但等会儿你推断出来了,就把它存到这个叫 R 的变量里,我好用它。”
条件类型为我们提供了一种在 true 分支中,使用 infer 关键字从我们比较的类型中推断出新类型的方法。
下面是经典案例:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;function getName(): string { return "John"; }function getAge(): number { return 25; }type NameType = ReturnType<typeof getName>; // stringtype AgeType = ReturnType<typeof getAge>; // number这里发生了什么?
- 我们检查
T是不是长得像一个函数(...args: any[]) => something
- 我们检查
- 如果像,我们就说:“不管那个 ‘something’ 是啥,都把它命名为
R”
- 如果像,我们就说:“不管那个 ‘something’ 是啥,都把它命名为
- 然后我们返回
R
- 然后我们返回
- 如果
T长得不像函数,就返回never
- 如果
我们来构建一个更实用的东西 —— 提取数组成员的类型:
type ArrayElement<T> = T extends (infer U)[] ? U : never;type StringArray = string[];type NumberArray = number[];type StringType = ArrayElement<StringArray>; // stringtype NumberType = ArrayElement<NumberArray>; // numbertype NotArray = ArrayElement<boolean>; // never接下来是见证奇迹的时刻 —— 提取组件的 props 类型:
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never;const MyButton: React.FC<{ label: string; onClick: () => void }> = (props) => ( <button onClick={props.onClick}>{props.label}</button>);type MyButtonProps = PropsOf<typeof MyButton>;// MyButtonProps is: { label: string; onClick: () => void }多个 infer 声明 也能一起用:
type FunctionInfo<T> = T extends (first: infer A, second: infer B) => infer R ? { args: [A, B]; return: R } : never;type LoginFunction = (username: string, password: string) => Promise<boolean>;type LoginInfo = FunctionInfo<LoginFunction>;// LoginInfo is: { args: [string, string]; return: Promise<boolean> }彩蛋规则:映射类型,从小处着手就不难
既然你已经搞懂了条件类型和 infer,那么映射类型对你来说就是小菜一碟了。
映射类型可以转换现有类型。 你可以把它想象成一个专门给类型属性用的 for...in 循环。
type Optional<T> = { [K in keyof T]?: T[K];}说人话就是: “对于类型 T 中的每一个属性 K,都创建一个同名的新属性,但让它是可选的,并且类型保持为 T[K] 不变。”
type User = { id: number; name: string; email: string;};type PartialUser = Optional<User>;// PartialUser is: {// id?: number;// name?: string;// email?: string;// }我们来整点更有意思的 —— 把所有属性的类型都变成字符串:
type Stringify<T> = { [K in keyof T]: string;}type StringifiedUser = Stringify<User>;// StringifiedUser is: {// id: string;// name: string;// email: string;// }映射类型与条件类型的梦幻联动:
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K;}[keyof T];type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;class UserService { id: number = 1; name: string = "John"; save(): void {} delete(): void {}}type UserData = NonFunctionProperties<UserService>;// UserData is: { id: number; name: string }// Methods are filtered out!终极合体:一个真实世界的例子
让我们来构建一个类型,它可以从 Redux 的 action 中提取出 payload 的类型:
// 我们的 action 类型们type LoginAction = { type: 'LOGIN'; payload: { username: string; password: string } };type LogoutAction = { type: 'LOGOUT'; payload: null };type UpdateProfileAction = { type: 'UPDATE_PROFILE'; payload: { name: string; email: string } };type Actions = LoginAction | LogoutAction | UpdateProfileAction;// 为特定的 action 提取 payload 类型type PayloadOf<T, ActionType extends string> = T extends { type: ActionType; payload: infer P } ? P : never;// 用法type LoginPayload = PayloadOf<Actions, 'LOGIN'>;// LoginPayload 的类型是: { username: string; password: string }type LogoutPayload = PayloadOf<Actions, 'LOGOUT'>;// LogoutPayload 的类型是: nulltype UpdatePayload = PayloadOf<Actions, 'UPDATE_PROFILE'>;// UpdatePayload 的类型是: { name: string; email: string }发生了什么:
- 我们利用条件类型的分发特性来检查联合类型中的每一个 action
- 当 action 类型匹配时,我们用
infer来提取 payload 的类型
- 当 action 类型匹配时,我们用
- 然后,TypeScript 就精准地把我们需要的 payload 类型交到我们手上了!
你的下一步修炼计划
既然你已经掌握了这三大法则,你就会开始在 TypeScript 内置的工具类型中,处处发现它们的影子:
•
Pick<T, K>uses mapped types•
Exclude<T, U>uses distributive conditional types•
ReturnType<T>usesinfer•
Parameters<T>combines conditional types withinfer
我给你下一个战书: 打开 TypeScript 的内置工具类型(在你编辑器的类型定义里就能找到),然后用我们今天学的这三条规则,去搞懂它们的工作原理。
从 Partial<T>、Required<T> 和 ReturnType<T> 开始。等这些都搞明白了,再向 Extract<T, U> 和 NonNullable<T> 进发。
总结一下
TypeScript 的高级类型不是什么黑魔法,它们就是三个简单的概念:
1. 条件类型 = 给类型用的 if-else
2. 分发行为 = 自动遍历联合类型
3. infer = 用模式匹配来提取类型
一旦你把这些模式内化于心,你就再也不会被复杂的类型定义吓到了。相反,你会开始发现各种机会,用它们来让你的代码变得更类型安全、更具表现力。
掌握高级类型不仅仅是为了在同事面前秀一把——它将彻底改变你建模和重构代码的方式。你能在编译时就捕获到那些可能需要花数小时在生产环境中调试的 bug。
准备好提升你的 TypeScript 功力了吗? 先从你常用的一个工具类型开始,搞懂它的底层原理,然后从零开始自己实现一个。
相信我,一旦你能流畅地读写这些类型,你会纳闷自己以前没它们是怎么活过来的。
你正在挑战哪个 TypeScript 类型难题?在评论区留言,我们一起解决它!如果这篇文章帮你揭开了高级类型的神秘面纱,请给它点个赞,并分享给那些还在类型地狱里挣扎的开发者伙伴们吧。
引用链接
[1] 《TypeScript Types That Scared Me — Until I Learned These 3 Rules》: _https://medium.com/the-syntax-diaries/typescript-types-that-scared-me-until-i-learned-these-3-rules-34f8ea09ecb2_[2] Amaresh Adak: _https://medium.com/@amareshadak_