TypeScript中有一项至关重要的进阶特性:conditional types
,这个功能出现之后,不少积压已久的TypeScript功能均可以垂手可得的实现了。html
那么本篇文章就会经过一个简单的功能:把git
distribute({ type: 'LOGIN', email: string }) 复制代码
这样的函数调用方式给简化为:github
distribute('LOGIN', { email: string }) 复制代码
没错,它只是节省了几个字符串,可是倒是一个很是适合咱们深刻学习条件类型的实战。typescript
先简单的看一个条件类型的示例:redux
function process<T extends string | null>( text: T ): T extends string ? string : null { ... } 复制代码
A extends B ? C : D 复制代码
这样的语法就叫作条件类型,A
, B
, C
和D
能够是任何类型表达式。api
这个extends
关键字是条件类型的核心。 A extends B
刚好意味着能够将类型A的任何值安全地分配给类型B的变量。在类型系统术语中,咱们能够说“ A可分配给B”。安全
从结构上来说,咱们能够说A extends B
,就像“ A是B的超集”,或者更确切地说,“ A具备B的全部特性,也许更多”。bash
举个例子来讲 { foo: number, bar: string } extends { foo: number }
是成立的,由于前者显然是后者的超集,比后者拥有更具体的类型。markdown
官方文档中,介绍了一种操做,叫 Distributive conditional types
app
简单来讲,传入给T extends U
中的T
若是是一个联合类型A | B | C
,则这个表达式会被展开成
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
复制代码
条件类型让你能够过滤联合类型的特定成员。 为了说明这一点,假设咱们有一个称为Animal的联合类型:
type Animal = Lion | Zebra | Tiger | Shark 复制代码
再假设咱们要编写一个类型,来过滤出Animal中属于“猫”的那些类型
type ExtractCat<A> = A extends { meow(): void } ? A : never type Cat = ExtractCat<Animal> // => Lion | Tiger 复制代码
接下来,Cat的计算过程会是这样子的:
type Cat = | ExtractCat<Lion> | ExtractCat<Zebra> | ExtractCat<Tiger> | ExtractCat<Shark> 复制代码
而后,它被计算成联合类型
type Cat = Lion | never | Tiger | never 复制代码
而后,联合类型中的never没什么意义,因此最后的结果的出来了:
type Cat = Lion | Tiger 复制代码
记住这样的计算过程,记住ts这个把联合类型如何分配给条件类型,接下来的实战中会颇有用。
举一个相似redux
中的dispatch
的例子。
首先,咱们有一个联合类型Action
,用来表示全部能够被dispatch接受的参数类型:
type Action = | { type: "INIT" } | { type: "SYNC" } | { type: "LOG_IN" emailAddress: string } | { type: "LOG_IN_SUCCESS" accessToken: string } 复制代码
而后咱们定义这个dispatch方法:
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变得更简单一点:
dispatch("LOG_IN_SUCCESS", { accessToken: "038fh239h923908h" }) 复制代码
好,推掉咱们的约会,打电话给咱们的女友!取消!
首先,利用方括号选择出Action
中的全部type
,这个技巧颇有用。
type ActionType = Action["type"] // => "INIT" | "SYNC" | "LOG_IN" | "LOG_IN_SUCCESS" 复制代码
可是第二个参数的类型取决于第一个参数。 咱们可使用类型变量来对该依赖关系建模。
declare function dispatch<T extends ActionType>( type: T, args: ExtractActionParameters<Action, T> ): void 复制代码
注意,这里就用到了extends
语法,规定了咱们的入参type
必须是ActionType
中一部分。
注意这里的第二个参数args,用ExtractActionParameters<Action, T>
这个类型来把type和args作了关联,
来看看ExtractActionParameters
是如何实现的:
type ExtractActionParameters<A, T> = A extends { type: T } ? A : never 复制代码
在此次实战中,咱们第一次运用到了条件类型,ExtractActionParameters<Action, T>
会按照咱们上文提到的分布条件类型
,把Action中的4项依次去和{ type: T }
进行比对,找出符合的那一项。
来看看如何使用它:
type Test = ExtractActionParameters<Action, "LOG_IN"> // => { type: "LOG_IN", emailAddress: string } 复制代码
这样就筛选出了type匹配的一项。
接下来咱们要把type去掉,第一个参数已是type了,所以咱们不想再额外声明type了。
// 把类型中key为"type"去掉 type ExcludeTypeField<A> = { [K in Exclude<keyof A, "type">]: A[K] } 复制代码
这里利用了keyof
语法,而且利用内置类型Exclude
把type
这个key去掉,所以只会留下额外的参数。
type Test = ExcludeTypeField<{ type: "LOG_IN", emailAddress: string }> // { emailAddress: string } 复制代码
而后用它来剔除参数中的 type
// 把参数对象中的type去掉 type ExtractActionParametersWithoutType<A, T> = ExcludeTypeField<ExtractActionParameters<A, T>>; 复制代码
declare function dispatch<T extends ActionType>( type: T, args: ExtractActionParametersWithoutType<Action, T> ): void 复制代码
到此为止,咱们就能够实现上文中提到的参数简化功能:
// ok dispatch({ type: "LOG_IN", emailAddress: "david.sheldrick@artsy.net" }) 复制代码
到了这一步为止,虽然带参数的Action能够完美支持了,可是对于"INIT"这种不须要传参的Action,咱们依然要写下面这样代码:
dispatch("INIT", {}) 复制代码
这确定是不能接受的!因此咱们要利用TypeScript的函数重载功能。
// 简单参数类型 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类型,
type SimpleAction = ExtractSimpleAction<Action>
复制代码
咱们如何定义这个ExtractSimpleAction
条件类型?
若是咱们从这个Action中删除type
字段,而且结果是一个空的接口,
那么这就是一个SimpleAction
。 因此咱们可能会凭直觉写出这样的代码:
type ExtractSimpleAction<A> = ExcludeTypeField<A> extends {} ? A : never 复制代码
但这样是行不通的,几乎全部的类型均可以extends {},由于{}太宽泛了。
咱们应该反过来写:
type ExtractSimpleAction<A> = {} extends ExcludeTypeField<A> ? A : never 复制代码
如今,若是ExcludeTypeField <A>
为空,则extends表达式为true,不然为false。
但这仍然行不通! 由于分布条件类型
仅在extends关键字的前面是类型变量时发生。
分布条件件类型仅发生在以下场景:
type Blah<Var> = Var extends Whatever ? A : B 复制代码
而不是:
type Blah<Var> = Foo<Var> extends Whatever ? A : B type Blah<Var> = Whatever extends Var ? A : B 复制代码
可是咱们能够经过一些小技巧绕过这个限制:
type ExtractSimpleAction<A> = A extends any ? {} extends ExcludeTypeField<A> ? A : never : never 复制代码
A extends any
是必定成立的,这只是用来绕过ts对于分布条件类型的限制,没错啊,咱们的A
确实是在extends的前面了,就是骗你TS,这里是分布条件类型。
而咱们真正想要作的条件判断被放在了中间,所以Action联合类型中的每一项又可以分布的去匹配了。
那么咱们就能够简单的筛选出全部不须要额外参数的type
type SimpleAction = ExtractSimpleAction<Action> type SimpleActionType = SimpleAction['type'] 复制代码
再利用Exclude取反,找到复杂类型:
type ComplexActionType = Exclude<ActionType, SimpleActionType> 复制代码
到此为止,咱们所须要的功能就完美实现了:
// 简单参数类型 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" }) 复制代码
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内置工具类型改造事后的源码,更加简洁优雅的完成了本文中的需求,能够扩展学习。