唠唠 TypeScript 分布式条件类型与 infer 及应用
(一)前言
想必大家或多或少都听过 TypeScript,而且应该也写过 TypeScript 代码。
不过可能来讲,在业务代码中用到的 TypeScript 的东西不会很多,更别说 TypeScript 类型编程了。
我个人除了在业务代码中编写 TypeScript 代码外,更多还是想在 TypeScript 的类型编程上持续学习。
而本文,更多是对自己阶段学习成果的整理输出,也希望文中提到的一些思路能够给大家些借鉴。
(二)条件类型与分布式条件类型
代码示例:conditional-type。
1. 条件类型
条件类型的语法和我们平时使用的三元表达式很类似,它们2者的基本语法如下:
// 三元表达式
A === B ? Result1 : Result2
// 条件类型
TypeA extends TypeB ? Result1 : Result2
条件类型中可以使用 extends
关键字来判断类型的兼容性(基于TypeScript类型层级系统),比如:
class {
() {
.('run...')
}
}
class extends {
() {
.('teach...')
}
}
class extends {
() {
.('study...')
}
}
type = extends ? true : false // true
type = extends ? true : false // true
上述例子中,Res1
和 Res2
的结果是 true
,从类型空间来看, Teacher
和 Student
是 Person
的子类型。知道这些类型关系,我们可以如下使用:
doSomething
函数定义 person
参数类型为 Person
即可,然后内部可以基于 instanceof
的结果收缩类型到具体子类型完成我们的代码逻辑。
类似的还有联合类型的类型兼容性比较:
type = 1 | 2 extends 1 | 2 | 3 ? true : false // true
type = 1 | 4 extends 1 | 2 | 3 ? true : false // false
不过绝大部分场景下,条件类型会和 泛型 一起使用,搭配使用可以完成的操作可太多了。
先来个简单🌰️:
type <> = extends string ? 'string' : 'number'
type = <'kai'> // "string"
type = <23> // "number"
上述例子中,在条件类型的基础上,基于填充后的泛型参数 T
,我们可以判断出传入的值的类型是 string
还是 number
。
当然,条件类型也是可以嵌套使用的:
type <> = extends string
? 'string'
: extends number
? 'number'
: extends boolean
? 'boolean'
: extends null
? 'null'
: extends undefined
? 'undefined'
: never;
type = <true> // "boolean"
type = <null> // "null"
type = <undefined> // "undefined"
type = <never> // "never"
除了简单的原始类型的类型比较外,我们还可以对更复杂的类型进行比较,来个🌰️:
type = (...: any[]) => any
type < extends > = extends (...: any[]) => string ? true : false
type = <(: string) => string> // true
type = <(: string) => boolean> // false
type = <() => number> // false
如上例子,我们可以通过 T extends (...args: any[]) => string ? true : false
条件类型判断一个函数的返回值类型是否是 string
。
2. 分布式条件类型
代码示例:distributive-conditional-type。
分布式条件类型,也称为条件类型的分布式特性,在满足一定的条件后即会触发,我把这些触发条件(同时满足)总结如下:
- 类型参数是联合类型;
- 类型参数通过泛型参数传入;
- 泛型参数位于条件类型左侧且不被包裹。
基于上述的触发条件,来看个🌰️:
type <> = extends 1 | 2 | 3 ? : never
type = <1 | 2 | 3 | 4> // 1 | 2 | 3
type = 1 | 2 | 3 | 4 extends 1 | 2 | 3 ? 1 | 2 | 3 | 4 : never // never
对于 Res1
,因为传入的联合类型符合触发的条件,因此触发了分布式特性,例子等价于:
type =
| (1 extends 1 | 2 | 3 ? 1 : never)
| (2 extends 1 | 2 | 3 ? 2 : never)
| (3 extends 1 | 2 | 3 ? 3 : never)
| (4 extends 1 | 2 | 3 ? 4 : never)
而 Res2
中的条件类型不满足触发分布式的条件,因此这里按类型兼容性进行比较,得到的结果就是 never
。
关于分布式条件类型的注意事项,神光 大佬写过一篇微信公众号文章,用几个例子说明了下,我觉得总结很不错了,这里就不再赘述,直接给出链接:条件类型的特殊情况。
这一小节的最后,给出几个个人觉得实用的工具类型:
type <> = [] extends [never] ? true : false
type = <never> // true
type = <any> // false
type = <boolean> // false
上述例子用 []
包裹了传入的泛型参数,避免触发了分布式特性,加上 never
只与自身存在类型兼容(any
就先不考虑),因此 IsNever
可以判断出是否为 never
类型。
type <> = 1 extends & 2 ? true : false
type = <any> // true
type = <never> // false
type = <boolean> // false
上述例子中,利用了 any
的特殊性,即 1 & any
交叉类型的结果会是 any
,加上 any
在TypeScript类型层级中,是所有类型的父类型,因此这里的 IsAny
可以判断出是否为 any
类型。
type <> = 1 extends & 2 ? true : false
type <> = unknown extends
? <> extends true
? false
: true
: false
type = <unknown> // true
type = <any> // false
type = <number> // false
上述例子中,由于TypeScript类型层级中,unknown
与 any
处于同一层级,2者互为父子类型,因此下边的结果都是 true
:
type = any extends unknown ? true : false // true
type = unknown extends any ? true : false // true
因此,unknown extends T
的结果可能为 any
或 unknown
,需要进一步通过上述的 IsAny
工具类型剔除 any
类型,最终判断出 unknown
类型。
(三)infer及其一些案例应用
代码示例:infer-case。
1. infer介绍
回到上边小节1. 条件类型中,我给出了一个判断函数返回参数是否为 string
类型的例子,如果现在不再这么比较,而是要拿到其返回值类型,我们可以怎么做?
此时,我们可以使用 infer
关键字,infer
可以在条件类型中提取类型的某一部分信息。
对于上述需求,可以这么写:
type = (...: any[]) => any
type < extends > = extends (...: any[]) => infer ? : never
type = <(: string) => string> // string
type = <(: string) => boolean> // boolean
type = <() => number> // number
对于上述例子,当传入的类型参数满足 T extends (...args: any[]) => infer R
这样一个结构(不用管 infer R
,当它是 any 就行)时,会返回 infer R
位置的值,即 R。否则,返回 never。
infer
是 inference
的缩写,意为推断,如 infer R
中 R
就表示 待推断的类型。
infer
只能在条件类型中使用,因为我们实际上仍然需要类型结构是一致的,比如上例中类型信息需要是一个函数类型结构,我们才能提取出它的返回值类型。如果连函数类型都不是,那只会返回 never 。
进一步说明下,infer
关键字的声明只允许在条件类型的 extends
子句中使用,否则会报错,来看个🌰️:
type MyType<T> = T extends string ? (infer U)[] : never; // Error
上述写法会报错,报错内容如下:
类型结构除了可以是函数类型外,还可以是其他的,比如数组类型,甚至于是 Promise,来看例子:
比如我想提取出数组中首尾2个元素的类型,可以如下写:
type < extends any[]> = extends [
infer ,
...any[],
infer
]
? [, ]
: never
// [string, number]
type = <[string, undefined, null, boolean, number]>
提取出Promise中resolve值的类型,可以如下写:
type <> = extends <infer > ? : never
type = <<number>> // number
type = <<undefined>> // undefined
当然了,infer
还有更多玩法,下边给出一些自己认为不错的工具类型实现,有些是 type-challenges 上的。
2. infer的一些案例应用
案例1:实现 TypeScript 内置工具类型 Parameters
和 ReturnType
:
type = (...: any[]) => any
type < extends > = extends (...: infer ) => any ? : never
type < extends > = extends (...: any[]) => infer ? : never
type = <(: string, : number) => string> // [name: string, age: number]
type = <(: string, : number) => string> // string
案例2:KebabCase
形式字符串转换为 CamelCase
形式字符串,比如 hello-world-kai
转换为 helloWorldKai
:
type < extends string> = extends `${infer }-${infer }`
? `${}${<<>>}`
:
type = <'hello-world-kai'> // "helloWorldKai"
因为 KebabCase
形式字符串可能存在多个 -
连接字符,因此这里还递归调用了 CamelCase
。
案例3:提取出数组的元素类型:
type < extends any[]> = extends <infer >
?
: never
// type Res13 = ArrayItemType<string> // 错误写法
type = <string[]> // string
type = <((: number) => string)[]> // (age: number) => string
type = <[number, boolean]> // number | boolean
// 等价于:
type = <(number | boolean)[]> // number | boolean
案例4:提取出接口中元素的类型:
type <, extends keyof > = extends { [ in ]: infer }
?
: never
interface Person {
: string
: number
: boolean
: string[]
}
type = <Person, 'name' | 'male'> // string | boolean
type = <Person, 'hobbies'> // string[]
这里 infer
关键字结合了映射类型和索引类型,实现了提取出接口中属性类型。
案例5:提取出数组中第一个字符串类型的元素:
type < extends any[]> = extends [
infer ,
...any[]
]
? extends string
?
: never
: never
type = <[12, 'kai', true]> // never
type = <['kai', 23, true]> // "kai"
而在 ts@4.7 版本后,引入了 infer 约束功能来实现对特定类型地提取,因此上述的 FirstArrayStringItem
可简写为:
type < extends any[]> = extends [
infer extends string,
...any[]
]
?
: never
type = <[12, 'kai', true]> // never
type = <['kai', 23, true]> // "kai"
案例6:深层提取出 Promise 中 resolve 值的类型
type <> = extends <infer > ? <> :
type = <<string>> // string
type = <<<'12'>>> // "12"
type = <<<<true>>>> // true
案例会持续更新!
(四)总结
本文结合一些示例介绍了条件类型、分布式条件类型和 infer
关键字的相关内容,也给出了一些工具类型实现,希望对大家有点帮助!
另外,基于条件类型和 infer
可以衍生出很多的类型编程写法,infer
关键字是类型编程模式匹配范式必不可少的工具,更多的内容感兴趣地可以自己去探索探索。
最后,给大家推荐个 Github仓库,是 antfu 搞的 TypeScript 类型编程练习仓库:type-challenges,感兴趣可以练练看~
欢迎各位大佬勘误~