TypeScript中有一项相当重要的进阶特性:conditional types
,这个功能出现以后,很多积压已久的TypeScript功能都可以轻而易举的实现了。
那么本篇文章就会通过一个简单的功能:把
1 2 3 4 | distribute({ type: 'LOGIN' , email: string }) |
这样的函数调用方式给简化为:
1 2 3 | distribute( 'LOGIN' , { email: string }) |
没错,它只是节省了几个字符串,但是却是一个非常适合我们深入学习条件类型的实战。
通过这篇文章,你可以学到以下特性在实战中是如何使用的:
- 🎉TypeScript的高级类型(Advanced Type)
- 🎉Conditional Types (条件类型)
- 🎉Distributive conditional types (分布条件类型)
- 🎉Mapped types(映射类型)
- 🎉函数重载
conditional types的第一次使用
先简单的看一个条件类型的示例:
1 2 3 4 5 | function process<T extends string | null >( text: T ): T extends string ? string : null { ... } |
A extends B ? C : D
这样的语法就叫做条件类型,A
, B
, C
和D
可以是任何类型表达式。
可分配性
这个extends
关键字是条件类型的核心。 A extends B
恰好意味着可以将类型A的任何值安全地分配给类型B的变量。在类型系统术语中,我们可以说“ A可分配给B”。
从结构上来讲,我们可以说A extends B
,就像“ A是B的超集”,或者更确切地说,“ A具有B的所有特性,也许更多”。
举个例子来说 { foo: number, bar: string } extends { foo: number }
是成立的,因为前者显然是后者的超集,比后者拥有更具体的类型。
分布条件类型
官方文档中,介绍了一种操作,叫 Distributive conditional types
简单来说,传入给T extends U
中的T
如果是一个联合类型A | B | C
,则这个表达式会被展开成
1 | (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y) |
条件类型让你可以过滤联合类型的特定成员。 为了说明这一点,假设我们有一个称为Animal的联合类型:
1 | type Animal = Lion | Zebra | Tiger | Shark |
再假设我们要编写一个类型,来过滤出Animal中属于“猫”的那些类型
1 2 3 4 | type ExtractCat<A> = A extends { meow(): void } ? A : never type Cat = ExtractCat<Animal> // => Lion | Tiger |
接下来,Cat的计算过程会是这样子的:
1 2 3 4 5 | type Cat = | ExtractCat<Lion> | ExtractCat<Zebra> | ExtractCat<Tiger> | ExtractCat<Shark> |
然后,它被计算成联合类型
1 | type Cat = Lion | never | Tiger | never |
然后,联合类型中的never没什么意义,所以最后的结果的出来了:
1 | type Cat = Lion | Tiger |
记住这样的计算过程,记住ts这个把联合类型如何分配给条件类型,接下来的实战中会很有用。
分布条件类型的真实用例
举一个类似redux
中的dispatch
的例子。
首先,我们有一个联合类型Action
,用来表示所有可以被dispatch接受的参数类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | type Action = | { type: "INIT" } | { type: "SYNC" } | { type: "LOG_IN" emailAddress: string } | { type: "LOG_IN_SUCCESS" accessToken: string } |
然后我们定义这个dispatch方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | declare function dispatch(action: Action): void // ok dispatch({ type: "INIT" }) // ok dispatch({ type: "LOG_IN" , emailAddress: "david.sheldrick@artsy.net" }) // ok dispatch({ type: "LOG_IN_SUCCESS" , accessToken: "038fh239h923908h" }) |
这个API是类型安全的,当TS识别到type为LOG_IN
的时候,它会要求你在参数中传入emailAddress
这个参数,这样才能完全满足联合类型中的其中一项。
到此为止,我们可以去和女朋友约会了,此文完结。
等等,我们好像可以让这个api变得更简单一点:
1 2 3 | dispatch( "LOG_IN_SUCCESS" , { accessToken: "038fh239h923908h" }) |
好,推掉我们的约会,打电话给我们的女朋友!取消!
参数简化实现
首先,利用方括号选择出Action
中的所有type
,这个技巧很有用。
1 2 | type ActionType = Action[ "type" ] // => "INIT" | "SYNC" | "LOG_IN" | "LOG_IN_SUCCESS" |
但是第二个参数的类型取决于第一个参数。 我们可以使用类型变量来对该依赖关系建模。
1 2 3 4 | declare function dispatch<T extends ActionType>( type: T, args: ExtractActionParameters<Action, T> ): void |
注意,这里就用到了extends
语法,规定了我们的入参type
必须是ActionType
中一部分。
注意这里的第二个参数args,用ExtractActionParameters<Action, T>
这个类型来把type和args做了关联,
来看看ExtractActionParameters
是如何实现的:
1 | type ExtractActionParameters<A, T> = A extends { type: T } ? A : never |
在这次实战中,我们第一次运用到了条件类型,ExtractActionParameters<Action, T>
会按照我们上文提到的分布条件类型
,把Action中的4项依次去和{ type: T }
进行比对,找出符合的那一项。
来看看如何使用它:
1 2 | type Test = ExtractActionParameters<Action, "LOG_IN" > // => { type: "LOG_IN", emailAddress: string } |
这样就筛选出了type匹配的一项。
接下来我们要把type去掉,第一个参数已经是type了,因此我们不想再额外声明type了。
1 2 | // 把类型中key为"type"去掉 type ExcludeTypeField<A> = { [K in Exclude<keyof A, "type" >]: A[K] } |
这里利用了keyof
语法,并且利用内置类型Exclude
把type
这个key去掉,因此只会留下额外的参数。
1 2 | type Test = ExcludeTypeField<{ type: "LOG_IN" , emailAddress: string }> // { emailAddress: string } |
然后用它来剔除参数中的 type
1 2 3 4 5 6 7 8 | // 把参数对象中的type去掉 type ExtractActionParametersWithoutType<A, T> = ExcludeTypeField<ExtractActionParameters<A, T>>; declare function dispatch<T extends ActionType>( type: T, args: ExtractActionParametersWithoutType<Action, T> ): void |
到此为止,我们就可以实现上文中提到的参数简化功能:
1 2 3 4 5 | // ok dispatch({ type: "LOG_IN" , emailAddress: "david.sheldrick@artsy.net" }) |
利用重载进一步优化
到了这一步为止,虽然带参数的Action可以完美支持了,但是对于"INIT"这种不需要传参的Action,我们依然要写下面这样代码:
dispatch("INIT", {})
这肯定是不能接受的!所以我们要利用TypeScript的函数重载功能。
1 2 3 4 5 6 7 8 9 10 11 | // 简单参数类型 function dispatch<T extends SimpleActionType>(type: T): void // 复杂参数类型 function dispatch<T extends ComplexActionType>( type: T, args: ExtractActionParametersWithoutType<Action, T>, ): void // 实现 function dispatch(arg: any, payload?: any) {} |
那么关键点就在于SimpleActionType
和ComplexActionType
要如何实现了,
SimpleActionType
顾名思义就是除了type以外不需要额外参数的Action类型,
1 | type SimpleAction = ExtractSimpleAction<Action> |
我们如何定义这个ExtractSimpleAction
条件类型?
如果我们从这个Action中删除type
字段,并且结果是一个空的接口,
那么这就是一个SimpleAction
。 所以我们可能会凭直觉写出这样的代码:
1 | type ExtractSimpleAction<A> = ExcludeTypeField<A> extends {} ? A : never |
但这样是行不通的,几乎所有的类型都可以extends {},因为{}太宽泛了。
我们应该反过来写:
1 | type ExtractSimpleAction<A> = {} extends ExcludeTypeField<A> ? A : never |
现在,如果ExcludeTypeField <A>
为空,则extends表达式为true,否则为false。
但这仍然行不通! 因为分布条件类型
仅在extends关键字的前面是类型变量时发生。
分布条件件类型仅发生在如下场景:
1 | type Blah<Var> = Var extends Whatever ? A : B |
而不是:
1 2 | type Blah<Var> = Foo<Var> extends Whatever ? A : B type Blah<Var> = Whatever extends Var ? A : B |
但是我们可以通过一些小技巧绕过这个限制:
1 2 3 4 5 | type ExtractSimpleAction<A> = A extends any ? {} extends ExcludeTypeField<A> ? A : never : never |
A extends any
是一定成立的,这只是用来绕过ts对于分布条件类型的限制,没错啊,我们的A
确实是在extends的前面了,就是骗你TS,这里是分布条件类型。
而我们真正想要做的条件判断被放在了中间,因此Action联合类型中的每一项又能够分布的去匹配了。
那么我们就可以简单的筛选出所有不需要额外参数的type
1 2 | type SimpleAction = ExtractSimpleAction<Action> type SimpleActionType = SimpleAction[ 'type' ] |
再利用Exclude取反,找到复杂类型:
1 | type ComplexActionType = Exclude<ActionType, SimpleActionType> |
到此为止,我们所需要的功能就完美实现了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 简单参数类型 function dispatch<T extends SimpleActionType>(type: T): void // 复杂参数类型 function dispatch<T extends ComplexActionType>( type: T, args: ExtractActionParameters<Action, T>, ): void // 实现 function dispatch(arg: any, payload?: any) {} // ok dispatch( "SYNC" ) // ok dispatch({ type: "LOG_IN" , emailAddress: "david.sheldrick@artsy.net" }) |
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | type Action = | { type: "INIT" ; } | { type: "SYNC" ; } | { type: "LOG_IN" ; emailAddress: string ; } | { type: "LOG_IN_SUCCESS" ; accessToken: string ; }; // 用类型查询查出Action中所有type的联合类型 type ActionType = Action[ "type" ]; // 把类型中key为"type"去掉 type ExcludeTypeField<A> = { [K in Exclude<keyof A, "type" >]: A[K] }; type ExtractActionParameters<A, T> = A extends { type: T } ? A : never // 把参数对象中的type去掉 // Extract<A, { type: T }会挑选出能extend { type: T }这个结构的Action中的类型 type ExtractActionParametersWithoutType<A, T> = ExcludeTypeField<ExtractActionParameters<A, T>>; type ExtractSimpleAction<A> = A extends any ? {} extends ExcludeTypeField<A> ? A : never : never; type SimpleActionType = ExtractSimpleAction<Action>[ "type" ]; type ComplexActionType = Exclude<ActionType, SimpleActionType>; // 简单参数类型 function dispatch<T extends SimpleActionType>(type: T): void ; // 复杂参数类型 function dispatch<T extends ComplexActionType>( type: T, args: ExtractActionParametersWithoutType<Action, T> ): void ; // 实现 function dispatch(arg: any, payload?: any) {} dispatch( "SYNC" ); dispatch( 'LOG_IN' , { emailAddress: 'ssh@qq.com' }) |
总结
本文的实战示例来自国外大佬的博客,我结合个人的理解整理成了这篇文章。
中间涉及到的一些进阶的知识点,如果小伙伴们不太熟悉的话,可以参考各类文档中的定义去反复研究,相信你会对TypeScript有更深一步的了解。
参考资料
源码
这里是用TS内置工具类型改造过后的源码,更加简洁优雅的完成了本文中的需求,可以扩展学习。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗