Skip to content

唠唠 TypeScript 分布式条件类型与 infer 及应用

(一)前言

想必大家或多或少都听过 TypeScript,而且应该也写过 TypeScript 代码。

不过可能来讲,在业务代码中用到的 TypeScript 的东西不会很多,更别说 TypeScript 类型编程了。

我个人除了在业务代码中编写 TypeScript 代码外,更多还是想在 TypeScript 的类型编程上持续学习。

而本文,更多是对自己阶段学习成果的整理输出,也希望文中提到的一些思路能够给大家些借鉴。

(二)条件类型与分布式条件类型

代码示例:conditional-type

1. 条件类型

条件类型的语法和我们平时使用的三元表达式很类似,它们2者的基本语法如下:

typescript
// 三元表达式
A === B ? Result1 : Result2

// 条件类型
TypeA extends TypeB ? Result1 : Result2

条件类型中可以使用 extends 关键字来判断类型的兼容性(基于TypeScript类型层级系统),比如:

typescript
class  {
   () {
    .('run...')
  }
}

class  extends  {
   () {
    .('teach...')
  }
}

class  extends  {
   () {
    .('study...')
  }
}

type  =  extends  ? true : false // true
type  =  extends  ? true : false // true

上述例子中,Res1Res2 的结果是 true,从类型空间来看, TeacherStudentPerson 的子类型。知道这些类型关系,我们可以如下使用:

image-20230727215357540

doSomething 函数定义 person 参数类型为 Person 即可,然后内部可以基于 instanceof 的结果收缩类型到具体子类型完成我们的代码逻辑。

类似的还有联合类型的类型兼容性比较:

typescript
type  = 1 | 2 extends 1 | 2 | 3 ? true : false // true
type  = 1 | 4 extends 1 | 2 | 3 ? true : false // false

不过绝大部分场景下,条件类型会和 泛型 一起使用,搭配使用可以完成的操作可太多了。

先来个简单🌰️:

typescript
type <> =  extends string ? 'string' : 'number'

type  = <'kai'> // "string"
type  = <23> // "number"

上述例子中,在条件类型的基础上,基于填充后的泛型参数 T ,我们可以判断出传入的值的类型是 string 还是 number

当然,条件类型也是可以嵌套使用的:

typescript
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"

除了简单的原始类型的类型比较外,我们还可以对更复杂的类型进行比较,来个🌰️:

typescript
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

分布式条件类型,也称为条件类型的分布式特性,在满足一定的条件后即会触发,我把这些触发条件(同时满足)总结如下:

  • 类型参数是联合类型;
  • 类型参数通过泛型参数传入;
  • 泛型参数位于条件类型左侧且不被包裹。

基于上述的触发条件,来看个🌰️:

typescript
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,因为传入的联合类型符合触发的条件,因此触发了分布式特性,例子等价于:

typescript
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

关于分布式条件类型的注意事项,神光 大佬写过一篇微信公众号文章,用几个例子说明了下,我觉得总结很不错了,这里就不再赘述,直接给出链接:条件类型的特殊情况

这一小节的最后,给出几个个人觉得实用的工具类型:

typescript
type <> = [] extends [never] ? true : false

type  = <never> // true
type  = <any> // false
type  = <boolean> // false

上述例子用 [] 包裹了传入的泛型参数,避免触发了分布式特性,加上 never 只与自身存在类型兼容(any就先不考虑),因此 IsNever 可以判断出是否为 never 类型。

typescript
type <> = 1 extends  & 2 ? true : false

type  = <any> // true
type  = <never> // false
type  = <boolean> // false

上述例子中,利用了 any 的特殊性,即 1 & any 交叉类型的结果会是 any,加上 any 在TypeScript类型层级中,是所有类型的父类型,因此这里的 IsAny 可以判断出是否为 any 类型。

typescript
type <> = 1 extends  & 2 ? true : false

type <> = unknown extends 
? <> extends true
  ? false
  : true
: false

type  = <unknown> // true
type  = <any> // false
type  = <number> // false

上述例子中,由于TypeScript类型层级中,unknownany 处于同一层级,2者互为父子类型,因此下边的结果都是 true

typescript
type  = any extends unknown ? true : false // true
type  = unknown extends any ? true : false // true

因此,unknown extends T 的结果可能为 anyunknown,需要进一步通过上述的 IsAny 工具类型剔除 any 类型,最终判断出 unknown 类型。

(三)infer及其一些案例应用

代码示例:infer-case

1. infer介绍

回到上边小节1. 条件类型中,我给出了一个判断函数返回参数是否为 string 类型的例子,如果现在不再这么比较,而是要拿到其返回值类型,我们可以怎么做?

此时,我们可以使用 infer 关键字,infer 可以在条件类型中提取类型的某一部分信息

对于上述需求,可以这么写:

typescript
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。

inferinference 的缩写,意为推断,如 infer RR 就表示 待推断的类型

infer 只能在条件类型中使用,因为我们实际上仍然需要类型结构是一致的,比如上例中类型信息需要是一个函数类型结构,我们才能提取出它的返回值类型。如果连函数类型都不是,那只会返回 never 。

进一步说明下,infer 关键字的声明只允许在条件类型的 extends 子句中使用,否则会报错,来看个🌰️:

typescript
type MyType<T> = T extends string ? (infer U)[] : never; // Error

上述写法会报错,报错内容如下:

image-20230727215523004

类型结构除了可以是函数类型外,还可以是其他的,比如数组类型,甚至于是 Promise,来看例子:

比如我想提取出数组中首尾2个元素的类型,可以如下写:

typescript
type < extends any[]> =  extends [
  infer ,
  ...any[],
  infer 
] 
? [, ]
: never

// [string, number]
type  = <[string, undefined, null, boolean, number]>

提取出Promise中resolve值的类型,可以如下写:

typescript
type <> =  extends <infer > ?  : never

type  = <<number>> // number
type  = <<undefined>> // undefined

当然了,infer 还有更多玩法,下边给出一些自己认为不错的工具类型实现,有些是 type-challenges 上的。

2. infer的一些案例应用

案例1:实现 TypeScript 内置工具类型 ParametersReturnType

typescript
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

typescript
type < extends string> =  extends `${infer }-${infer }`
? `${}${<<>>}`
: 

type  = <'hello-world-kai'> // "helloWorldKai"

因为 KebabCase 形式字符串可能存在多个 - 连接字符,因此这里还递归调用了 CamelCase

案例3:提取出数组的元素类型:

typescript
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:提取出接口中元素的类型:

typescript
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:提取出数组中第一个字符串类型的元素:

typescript
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 可简写为:

typescript
type < extends any[]> =  extends [
  infer  extends string,
  ...any[]
]
? 
: never

type  = <[12, 'kai', true]> // never
type  = <['kai', 23, true]> // "kai"

案例6:深层提取出 Promise 中 resolve 值的类型

typescript
type <> =  extends <infer > ? <> : 

type  = <<string>> // string
type  = <<<'12'>>> // "12"
type  = <<<<true>>>> // true

案例会持续更新!

(四)总结

本文结合一些示例介绍了条件类型、分布式条件类型和 infer 关键字的相关内容,也给出了一些工具类型实现,希望对大家有点帮助!

另外,基于条件类型和 infer 可以衍生出很多的类型编程写法,infer 关键字是类型编程模式匹配范式必不可少的工具,更多的内容感兴趣地可以自己去探索探索。

最后,给大家推荐个 Github仓库,是 antfu 搞的 TypeScript 类型编程练习仓库:type-challenges,感兴趣可以练练看~

欢迎各位大佬勘误~

(五)参考资料