特殊类型判断

135 阅读7分钟
  • any 类型与任何类型的交叉都是 any,也就是 1 & any 结果是 any,可以用这个特性判断 any 类型。
  • 联合类型作为类型参数出现在条件类型左侧时,会分散成单个类型传入,最后合并。
  • never 作为类型参数出现在条件类型左侧时,会直接返回 never。
  • any 作为类型参数出现在条件类型左侧时,会直接返回 trueType 和 falseType 的联合类型。
  • 元组类型也是数组类型,但每个元素都是只读的,并且 length 是数字字面量,而数组的 length 是 number。可以用来判断元组类型。
  • 函数参数处会发生逆变,可以用来实现联合类型转交叉类型。
  • 可选索引的索引可能没有,那 Pick 出来的就可能是 {},可以用来过滤可选索引,反过来也可以过滤非可选索引。
  • 索引类型的索引为字符串字面量类型,而可索引签名不是,可以用这个特性过滤掉可索引签名。
  • keyof 只能拿到 class 的 public 的索引,可以用来过滤出 public 的属性。
  • 默认推导出来的不是字面量类型,加上 as const 可以推导出字面量类型,但带有 readonly 修饰,这样模式匹配的时候也得加上 readonly 才行。

1、any 类型

any 类型与任何类型的交叉都是 any,也就是 1 & any 结果是 any。

type IsAny37<T> = 'lily' extends ('hello' & T) ? true : false
//这里的 'lily' 和 'hello' 可以换成任意两个不同的类型。

//当传入 any 时
type test1IsAny37 = IsAny37<any>;
//type test1IsAny37 = true

//当传入其他类型时
type test2IsAny37 = IsAny37<number>;
//type test2IsAny37 = false

2、IsEqual

stackoverflow.com/questions/6…

type IsEqual37_1<A, B> = (A extends B ? true : false) & (B extends A ? true : false);
//因为 any 可以是任何类型,任何类型也都是 any,所以当这样写判断不出 any 类型来。
type testIsEqual37_1 = IsEqual37_1<'a', any>;
//type testIsEqual37_1 = true

//可以改成下面这样
///??但是我没看懂T是在哪里被赋值的
type IsEqual37_2<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;

type testIsEqual37_2 = IsEqual37_2<'a', any>;
//type testIsEqual37_2 = false

3、判断联合类型

//A extends A 这段看似没啥意义,主要是为了触发分布式条件类型,让 A 的每个类型单独传入。
//[B] extends [A] 这样不直接写 B 就可以避免触发分布式条件类型,那么 B 就是整个联合类型。
//比如下面:
type TestUnion36<A, B = A> = A extends A ? { a: A, b: B } : never;
type TestUnionResult36 = TestUnion36<'a' | 'b' | 'c'>;
/*
type TestUnionResult36 = {
    a: "a";
    b: "a" | "b" | "c";
} | {
    a: "b";
    b: "a" | "b" | "c";
} | {
    a: "c";
    b: "a" | "b" | "c";
}
A 和 B 都是同一个联合类型,为啥值还不一样呢?
因为条件类型中如果左边的类型是联合类型,会把每个元素单独传入做计算,而右边不会。
*/

//那么利用这个特点就可以实现 Union 类型的判断:
type IsUnion36<A, B = A> =
  A extends A
  ? [B] extends [A]
  ? false
  : true
  : never
//B 是联合类型整体,而 A 是单个类型,自然不成立
//而其它类型没有这种特殊处理,A 和 B 都是同一个,怎么判断都成立。
//利用这个特点就可以判断出是否是联合类型。

4、判断never类型

never 在条件类型中也比较特殊,如果条件类型左边是类型参数,并且传入的是 never,那么直接返回 never:

type TestNever37<T> = T extends number ? 1 : 2;
type testTestNever37 = TestNever37<never>;
//type testTestNever37 = never

所以,要判断 never 类型,就不能直接 T extends number,可以这样写:

type IsNever37<T> = [T] extends [never] ? true : false
type testIsNever37 = IsNever37<never>;
//type testIsNever37 = true

5、判断元组类型

元组类型也是数组类型,但每个元素都是只读的,并且 length 是数字字面量

而数组的 length 是 number。

元组和数组的 length 属性值是有区别的:

type lengthOfTuple37=[1,2,3]['length'];
//type lengthOfTuple37 = 3

type lengthofArray37=number[]['length'];
//type lengthofArray37 = number

判断元组类型根据length:

type NotEqual37<A, B> =
  (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
  ? false : true;
//A 是 B 类型,并且 B 也是 A 类型,那么就是同一个类型,返回 false,否则返回 true。

//判断元组
type IsTuple37<T> =
  T extends readonly [...params: infer Eles]
  ? NotEqual37<Eles['length'], number>
  : false
type lengthOfTuple37_1 = IsTuple37<[1, 2, 3]>
//type lengthOfTuple37_1 = true

type lengthofArray37_2 = IsTuple37<number[]>
//type lengthOfTuple37_1 = true

6、联合转交叉

类型之间是有父子关系的,更具体的那个是子类型,比如 A 和 B 的交叉类型 A & B 就是联合类型 A | B 的子类型,因为更具体。

如果允许父类型赋值给子类型,就叫做逆变

如果允许子类型赋值给父类型,就叫做协变

函数参数的逆变性质一般就联合类型转交叉类型会用

联合类型转交叉类型: zhuanlan.zhihu.com/p/503184171

//联合类型转交叉类型
type UnionToIntersection37<U> =
  (U extends U ? (x: U) => unknown : never) extends (x: infer R) => unknown
  ? R
  : never
//U extends U 是为了触发联合类型的 distributive 的性质
//让每个类型单独传入做计算,最后合并。

type testUnionToIntersection37 = UnionToIntersection37<{ a: 1 } | { b: 2 }>
//type testUnionToIntersection37 = {a: 1;} & {b: 2;}

7、提取索引类型中的可选索引

Pick 是 ts 提供的内置高级类型,就是取出某个 Key 构造新的索引类型:

type Pick<T, K extends keyof T> = { [P in K]: T[P]; }

type obj37 = { name: string; gender?: number | string };
type testPick = Pick<obj37, 'name'>;
//type testPick = {name: string;}

提取索引类型中的可选索引

type obj37 = { name: string; gender?: number | string };
type GetOptional37<Obj extends Record<string, any>> = {
  [
  Key in keyof Obj
  as {} extends Pick<Obj, Key> ? Key : never
  ]: Obj[Key];
}
//类型参数 Obj 为待处理的索引类型
//类型约束为索引为 string、值为任意类型的索引类型 Record<string, any>。
//索引是之前的索引也就是 Key in keyof Obj
//但要做一些过滤,也就是 as 之后的部分
//过滤的方式就是单独取出该索引之后,判断空对象是否是其子类型

type testGetOptional37 = GetOptional37<obj37>;
/*
type testGetOptional37 = {
  gender?: string | number | undefined;
}
*/

8、过滤所有非可选的索引

type obj37 = { name: string; gender?: number | string }; 
type isRequired37<Key extends keyof Obj, Obj> = 
    {} extends Pick<Obj, Key> ? never : Key;

type GetRequired37<Obj extends Record<string, any>> = { 
    [Key in keyof Obj as isRequired37<Key, Obj>]: Obj[Key] 
}
type testGetRequired37=GetRequired37<obj37>;
/*
type testGetRequired37 = {
    name: string;
}

9、删除索引类型中的可索引签名

这里的 sleep 是具体的索引 [key: string]: any 就是可索引签名,代表可以添加任意个 string 类型的索引。

type obj37_1 = {
  [key: string]: any;
  sleep(): void;
}

想删除索引类型中的可索引签名:索引签名不能构造成字符串字面量类型,因为它没有名字,而其他索引可以。

//索引签名不能构造成字符串字面量类型,因为它没有名字,而其他索引可以。
type obj37_1 = {
  [key: string]: any;
  sleep(): void;
}

type RemoveIndexSignature37<Obj extends Record<string, any>> = {
  [
  Key in keyof Obj
  as Key extends `${infer Str}` ? Str : never
  ]: Obj[Key]
}
//类型参数 Obj 是待处理的索引类型,约束为 Record<string, any>。
//通过映射类型语法构造新的索引类型,索引是之前的索引 Key in keyof Obj,但要做一些过滤,也就是 as 之后的部分。
//如果索引是字符串字面量类型,那么就保留,否则返回 never,代表过滤掉。


type testRemoveIndexSignature37 = RemoveIndexSignature37<obj37_1>;
/*
type testRemoveIndexSignature37 = {
    sleep: () => void;
}
*/

10、滤出 class 的 public 的属性

keyof 只能拿到 class 的 public 索引,private 和 protected 的索引会被忽略

class demo37 {
  public name: string;
  protected age: number;
  private hobbies: string[];

  constructor() {
    this.name = 'hello';
    this.age = 18;
    this.hobbies = ['sleep', 'read'];
  }
}

//keyof 拿到的只有 name:
type publicKeydemo37 = keyof demo37;
//type publicKeydemo37 = "name"

我们就可以根据这个特性实现 public 索引的过滤

type ClassPublicProps37<Obj extends Record<string, any>> = {
  [Key in keyof Obj]: Obj[Key]
}
type testClassPublicProps37 = ClassPublicProps37<demo37>;
/*
type testClassPublicProps37 = {
    name: string;
}
*/

11、as const

TypeScript 默认推导出来的类型并不是字面量类型。比如:

//对象:
const obj37_2 = {
  a: 1,
  b: 'hello',
  c: true
}
type obj37_2Type = typeof obj37_2
/*
type obj37_2Type = {
    a: number;
    b: string;
    c: boolean;
}
*/


//数组:
const arr37 = [1,2,3]
type arr37Type = typeof arr37
//type arr37Type = number[]
*/

需要推导出字面量类型的话应该加上as const:

const 是常量的意思,也就是说这个变量首先是一个字面量值,而且还不可修改,有字面量和 readonly 两重含义。所以加上 as const 会推导出 readonly 的字面量类型。

//对象:
const obj37_3 = {
  a: 1,
  b: 'hello',
  c: true
} as const

type obj37_3Type = typeof obj37_3
/*
type obj37_3Type = {
    readonly a: 1;
    readonly b: "hello";
    readonly c: true;
}
*/

//数组:
const arr37_1 = [1, 2, 3] as const
type arr37_1Type = typeof arr37_1
//type arr37_1Type = readonly [1, 2, 3]

反转三个元素的元组:

//不加上 readonly 再匹配是匹配不出来的
type ReverseArr37<Arr> = Arr extends readonly [infer A, infer B, infer C] ? [C, B, A] : never;
type testTuple37 = [1, 2, 3];
type testReverseArr37 = ReverseArr37<testTuple37>;
//type testReverseArr37 = [3, 2, 1]

类型体操顺口溜

模式匹配做提取,重新构造做变换。

递归复用做循环,数组长度做计数。

联合分散可简化,特殊特性要记清。

基础扎实套路熟,类型体操可通关。

原文:

juejin.cn/book/704752…