通过前面的学习, 我们知道了条件类型可以用来判断类型的兼容性, 从而确定类型的层级, 本节我们来看下统一学习下它的特性;
泛型
通过前面的学习,我们知道了, 泛型和类型工具通常能组合在一起, 构成一个工具类型, 而条件类型作为一种重要的类型工具, 往往能起非常大的作用, 最简单的, 我们可以用它来判断泛型类型, 来实现不同的逻辑
type IsString<T> = T extends string ? 'isString' : 'isNotString'
type NewType1 = IsString<number> // 'isNotString'
type NewType2 = IsString<string> // 'isString'
注意上面的泛型T其实是在调用的时候显示赋值的, 也就是string, number这些, 就像是这个工具类型的参数一样; 但是, 在函数中, 通常又可以隐式赋值居多, 通过函数的参数, 反过来确定泛型的类型
function fn<T>(x: T, y:T):T {
return x + (y as any)
}
fn('1', '2') // "1"|"2"
以上案例中, 之所以会返回一个联合类型, 是因为我们的两个参数分别对T这个泛型进行了隐式赋值; 而这种赋值是不受约束的, 我写什么参数都行, 反正T能接受任意类型! 那如果我们想将泛型T限制为特定的某种类型, 又要如何处理呢? 同样, 可以使用extends关键字:
// 函数泛型隐式赋值
function fn<T extends 1|2|3>(x: T, y:T):T {
return x + (y as any)
}
const result = fn(1,2) // 1|2
// 工具类型泛型显式赋值, 比如Typescript中内置的工具类型Record
type Record<K extends keyof any, T> = {
[P in K]: T
}
type K = Record<string, unknown>
/**
* type K = {
* [x: string]: unknown;
* }
*/
注意, 以上案例中的extends和前面在工具类型逻辑中的extends表达的意思有点不同, 之前的条件类型是一种类型逻辑的判断; 此处<>中的T extends 其实是一种类型约束; 通过这种约束, T被限定为是1|2|3这个联合类型的子类; K被限定为是any的索引类型查询的子类型(K extends keyof any); 由此, 我们知道, 泛型可以在调用时被显式/隐式赋值, 我们也能够通过extends关键字来约束泛型的值
infer
上面, 我们都是将一些简单的类型赋给泛型, 条件判断的逻辑也仅仅只是这个泛型是否为某某类型, 现实开发中当然不可能都是那么简单的场景; 假如我想获取泛型的某部分类型, 又当如何处理? 比如, 我的泛型是一个函数类型, 我想获取这个函数类型的返回值是啥类型, 此时, 就可以用到infer关键字, 它能够获取条件类型中, 泛型类型(或者说复杂结构类型)的任意部分的类型
// 获取函数的返回类型
type ReturnValueType<T> = T extends (...params: any[]) => infer R ? R : never
type Result1 = ReturnValueType<(a:string) => string> // string
type Result2 = ReturnValueType<(a:string) => number> // number
type Person = {
name: string;
height: number;
gender: 'male' | 'female'
}
// 获取一个对象类型中的值类型
type ValueType<T> = T extends {[key:string]: infer Value} ? Value : never
type Result3 = ValueType<Person> // string | number
// 获取对象类型中键的类型
type KeyTypes<T> = T extends Record<infer K, unknown> ? K : never
type Result4 = KeyTypes<Person> // name | height | gender
// 获取数组的第一位的类型
type FirstItemType<T> = T extends [infer R, ...any[]] ? R : never
type Result5 = FirstItemType<[string, number, boolean]> // string
// 交换数组首尾项
type RevertArrFirstLast<T> = T extends [infer F, ...any[], infer L] ? [L, ...any[], F] : never
type Result6 = RevertArrFirstLast<[string, number, boolean]> // [boolean, number, string]
// 交换键值类型
type RevertKeyValue<T extends Record<string, string>> = T extends Record<infer K, infer V> ? Record<V & string, K> : never
// 注意, 此处之所以要进行 V & string, 是因为V可能为其他类型, 即 不是string | number 中的任何一个
type Result7 = RevertKeyValue<{'age': '18'}> // {18: 'age'}
注意此处的RevertKeyValue中, 使用了V & string这个技巧, 将其转为string的交叉类型, 之所以要这么做, 是因为只有string | number | symbol可以为键的类型; 而此处infer对键值进行推导; 而由于Typescript在此场景下, 缺失了从之前的值那里继承string类型的特性, 从而导致了此错误, 此处, 则需要使用交叉类型来规避;
条件类型分布式特性
我们已经很熟悉条件类型的使用了, 不过它还有一个很有意思的特性, 即分布式特性, 我们将来学习下, 所谓条件类型的分布式特性, 指的是, 当我们使用条件类型时, 当被检查类型是一个联合类型时, 在一定条件下 , 条件类型将会'分布'到各个成员, 对各个成员分别进行条件判断, 然后再次返回一个联合类型的特性;
我们先来看看案例:
type union<T> = T extends 1 | 2 | 3 | 7 | 8 ? T : 'no'
type Result1 = union<1 | 2 | 3 | 4 | 5> // 1 | 2 | 3 | "no"
type numberExtends1<T> = T extends number ? 'yes' : 'no'
type Result2 = numberExtends1<123 | '2' | true> // no | yes
以上案例中, 联合类型被检查成员被依次进行了条件判断, 最后将每次判断的结果组成了一个新的联合类型返回, 这就是条件类型的分布式特性 , 或者也可以说, 这是一个分布式条件类型; 是不是感觉, 只要一个联合类型 extends 另一个联合类型, 只要有多个相同成员, 就一定能返回一个新的联合类型? 就能触发分布式特性? 接着看:
type Result1 = 1 | 2 | 3 | 4 | 5 extends 1 | 2 | 3 | 7 | 8 ? 1 | 2 | 3 | 4 | 5 : "no" // "no"
type numberExtends2<T> = [T] extends [number] ? 'yes' : 'no'
type Result2 = numberExtends2<123 | '2' | true> // "no"
可以看到, 当我们把联合类型直接拿出来比较, 或者将泛型参数进行数组符号包裹, 分布式特性就失效了! 就变成了一个联合类型作为一个整体 extends 另一个联合类型! 只会返回一个结果, 即是或否! 由此我们可以知道, 条件类型的分布式特性想要起作用, 必须符合至少2条件: 1. 联合类型必须是以泛型的形式传入比较; 2. 传进来的泛型最好直接拿来判断, 而不要对其进行其他操作, 这些操作, 诸如放入数组或是求交叉类型等, 都有可能导致分布式特性失效;
type Union1<T> = T & {} extends 'jack' | 'rose' | 'ship' ? T : 'no'
type Result1 = Union1<'jack' | 'ship' | 'ice'> // "no"
type Union2<T> = T & number extends 1 | 2 | 3 ? 'yes' : 'no'
type Result2 = Union2<1 | 3 | 4> // "no"
type Union3<T> = (T & T) extends 1 | 2 | 3 ? 'yes' : 'no'
type Result3 = Union2<1 | 3 | 4> // "no"
可以看出, 有时候, 即使T和某些类型的交叉类型就是T本身, 一样会让分布式特性失效!
any & never
关于any, 前面介绍类型层级的时候, 已经展示过了, any extends 任意非any和unknown类型, 其结果都是一个联合类型, 即 是 与 否 逻辑结果组成的联合类型!
type Result1 = any extends string ? 'yes' : 'no'
type Result2 = any extends number ? 'yes' : 'no'
type Result8 = any extends bigint ? 'yes' : 'no'
type Result3 = any extends boolean ? 'yes' : 'no'
type Result5 = any extends never ? 'yes' : 'no'
type Result4 = any extends object ? 'yes' : 'no'
type Result6 = any extends {} ? 'yes' : 'no'
type Result7 = any extends Object ? 'yes' : 'no'
// 以上结果均为'yes' | 'no'
// 如果将其放入工具类型, 通过泛型来传参的时候, 结果也一样
type AnyExString<T> = T extends string ? 'yes' : 'no'
type Result8 = AnyExString<any> // "yes" | "no"
type AnyExNumber<T> = T extends number ? 'yes' : 'no'
type Result9 = AnyExNumber<any> // "yes" | "no"
// ...
由此可以得知, any在进行条件类型判断时无论直接判断还是以泛型形式传入, 结果都一样!
而never使用条件类型时, 却存在不同的特性
type Result1 = never extends string ? 'yes' : 'no' // yes
type NeverFn<T> = T extends string ? 'yes' : 'no'
type Result2 = NeverFn<never> // never
可以看出, never进行条件类型判断时, 如果是直接判断, 则会进行正常的逻辑判断; 而如果是以泛型的形式进行判断时, 会直接跳过判断, 返回一个never; 那如果我们想写一个判断一个类型是否是never的工具类型, 该怎么写?来看看以下案例:
type isNever1<T> = [T] extends [never] ? true : false
type Result1 = isNever<never> // true
type isNever2<T> = {name: T} extends {name:never} ? true : false
type Result2 = isNever<never> // true
以上案例中, 如果我们将泛型参数作为另一个类型结构的一部分, 就能利用结构化类型系统的特性, 以另一个类型来比较了, 这样就能判断never类型了
既然如此, 那么如何判断一个类型是否是any? any似乎是一个很'狡猾'的家伙, 让人捉摸不透, 其实我们可以利用它的这一特性, 来帮助我们找到判断它的办法
type isTrue = 'str' extends number ? true : false
看看上面的案例, isTrue的结果肯定是false, 毕竟'str'怎么可能是number的子类? 那它会是谁的子类? 那自然是string、Object、String和any的子类, 那我们想办法把number变成这几种类型不就行了? 问题是怎么变? 要把'str'变成string、Object、String, 只能是断言这种开挂的方式, 如果不用断言; 那基本没办法了; 所以, 只有将其转为any, 如何转呢? 我们可以使用交叉类型
type Result1 = number & any // any
所以, 可以利用这种特性:
type isAny<T> = 'str' extends number & T ? true : false
type Result1 = isAny<any> // true
这样, 就有了判断是否为any的办法了;
那么, 又该如何判断unknown类型呢? 它又有什么特性, 不错, 只有它能和any‘称兄道弟’! 即互相extends结果均为true;
type isTrue1 = any extends unknown ? 'yes' : 'no' // 'yes'
type isTrue2 = unknown extends any ? 'yes' : 'no' // 'yes'
所以
type isAny<T> = 'str' extends number & T ? true : false
type isUnknown<T> = unknown extends T ? isAny<T> extends true ? false : true : false
type Result = isUnknown<unknown> // true
我们知道, unknown extends 只有在跟上any 和 unknown自己的时候, 才能返回'true'的逻辑; 而我们接着判断它是否为any, isAny extends true, 如果成立, 则说明这只是一个any类型, 则返回false, 否则, 就返回true; 通过这套逻辑, 我们就成功判断了一个类型是否为unknown了;