type-challenges系列(番外):技巧与知识点

这篇文章总结了在做 TypeScript 类型挑战(type-challenges)题目过程中积累的一些技巧和知识点,便于后续查阅和复习。

类型操作符

keyof

keyof 操作符接受一个对象类型作为参数,返回该对象属性名组成的字面量联合类型

ts 复制代码
type MyObj = {
  foo: number;
  bar: string;
};

type Keys = keyof MyObj; // 'foo'|'bar'

由于 JavaScript 对象的键名只有三种类型,所以对于any的键名的联合类型就是 string|number|symbol

ts 复制代码
// string | number | symbol
type KeyT = keyof any;

对于没有自定义键名的类型使用 keyof 运算符,返回never 类型,表示不可能有这样类型的键名

ts 复制代码
type KeyT = keyof object; // never

对于联合类型,keyof 返回成员共有的键名

ts 复制代码
type A = { a: string; z: boolean };
type B = { b: string; z: boolean };

// 返回 'z'
type KeyT = keyof (A | B);

对于交叉类型,keyof 返回所有键名

ts 复制代码
type A = { a: string; x: boolean };
type B = { b: string; y: number };

// 返回 'a' | 'x' | 'b' | 'y'
type KeyT = keyof (A & B);

// 相当于
keyof (A & B) ≡ keyof A | keyof B

它的用法主要有两个:

用法一:与extends一起使用,对对象属性的类型做限定

比如:

ts 复制代码
function prop<Obj, K extends keyof Obj>(obj: Obj, key: K): Obj[K] {
  return obj[key];
}

上面代码对函数prop的参数做了规定:需要传两个参数,并且第二个参数key必须是第一个参数obj的属性值

这样使用不会报错:

ts 复制代码
prop({ a: 1 }, "a");

但是这样使用就会报错了:

ts 复制代码
prop({ a: 1 }, "b"); //Argument of type '"b"' is not assignable to parameter of type '"a"'.(2345)

用法二:与in搭配进行属性映射,将一个类型的所有属性逐一映射成其他值

比如:

ts 复制代码
type NewProps<Obj> = {
  [Prop in keyof Obj]: boolean;
};

// 用法
type MyObj = { foo: number; bar: string };

// 等于 { foo: boolean; bar: boolean }
type NewObj = NewProps<MyObj>;

NewProps 类型可以将传入类型对象Obj的所有属性值类型都转化为boolean

in

in的右侧一般会跟一个联合类型,使用 in 操作符可以对该联合类型进行迭代。 其作用类似 JS 中的 for...in 或者 for...of

ts 复制代码
type Animals = "pig" | "cat" | "dog";
type animals = {
  [key in Animals]: string;
};
// type animals = {
//     pig: string; //第一次迭代
//     cat: string; //第二次迭代
//     dog: string; //第三次迭代
// }

typeof

typeof 操作符用于获取一个 JavaScript 变量的类型,常用于获取一个普通对象或者一个函数的类型

基本使用

假如我们在定义类型之前已经有了对象 obj,就可以用 typeof 来定义一个类型

ts 复制代码
const p = {
  name: "CJ",
  age: 18,
};

type Person = typeof p;

// 等同于
type Person = {
  name: string;
  age: number;
};

获取嵌套对象类型

如果对象是一个嵌套的对象,typeof 也能够正确获取到它们的类型。

ts 复制代码
const p = {
  name: "CJ",
  age: 18,
  address: {
    city: "SH",
  },
};

type Person = typeof p;

// 相当于
type Person = {
  name: string;
  age: number;
  address: {
    city: string;
  };
};

获取数组类型

假如我们有一个字符串数组,可以把数组的所有元素组合成一个新的类型:

ts 复制代码
const data = ["hello", "world"] as const;
type Greeting = (typeof data)[number];

// type Greeting = "hello" | "world"

获取函数类型

函数也是一个特殊的对象,typeof当然也能获取函数类型

ts 复制代码
function add(a: number, b: number): number {
  return a + b;
}
type AddFn = typeof add;
// AddFn: (a: number, b: number) => number

typeofkeyof 的区别:

关键字 作用 用法 结果类型
typeof 获取"值"的类型 typeof 变量名 一个类型
keyof 获取"类型"的所有键名 keyof 类型名 联合类型(键名)

extends

extends 关键词一般有两种用法:条件类型类型约束

条件类型

条件类型 类似于 JavaScript 中的三元表达式,可以根据当前类型是否符合某种条件,返回不同的类型

ts 复制代码
T extends U ? X : Y

上面式子中的extends用来判断,类型T是否可以赋值给类型U,即T是否为U的子类型,这里的TU可以是任意类型。

举个例子:

ts{1,2} 复制代码
type IsBoolean<T> = T extends boolean ? true : false
type IsArray<T> = T extends { length: number } ? true : false

type Res1 = IsBoolean<string>   // false
type Res2 = IsBoolean<true>     // true
type Res3 = IsBoolean<true>     // false
type Res4 = IsArray<[1, 2]>     // true

条件类型还有一种用法:当只有extends 右侧有联合类型,左侧没有联合类型时,虽然确实会进行多次判断,但是其多次判断的结果会以或的方式合并后交由extends的逻辑处理

举个例子:

ts 复制代码
'a' extends 'a' | 'b' ? 1 : 2

具体的流程

  1. a extends a 返回为 true
  2. a extends b 返回为 false
  3. 两个结果进行或判断即true | falsetrue,所以最终结果返回1

分布式条件类型

在条件类型中有一个特别需要注意的东西就是:分布式条件类型 (对联合类型应用 extends 时,会遍历联合类型成员并一一应用该条件类型)

ts 复制代码
// 内置工具:交集
type Extract<T, U> = T extends U ? T : never;
type type1 = "name" | "age";
type type2 = "name" | "address" | "sex";

type test = Extract<type1, type2>;
// 结果为 'name'

代码详解

  • T extends U ? T : never:因为 T 是一个联合类型,所以这里适用于分布式条件类型的概念。根据其概念,在实际的过程中会把 T 类型中的每一个子类型进行迭代
ts 复制代码
// 初始状态
'name' | 'age' extends 'name' | 'address' | 'sex' ? T : never

// 第一次迭代使用'name',得到 'name'
'name' extends 'name' | 'address' | 'sex' ? 'name' : never
// 第二次迭代使用`age`,得到 never
'age' extends 'name' | 'address' | 'sex' : never
  • 在迭代完成之后,会把每次迭代的结果组合成一个新的联合类型(根据 never 类型的特点,最后的结果会剔除掉 never
ts 复制代码
type result = "name" | never;
// 实际为 type result = 'name'

infer

infer 关键词的作用是延时推导 ,它会在类型未推导时进行占位,等到真正推导成功后再返回正确的类型,它用来定义泛型里面推断出的类型参数,它通常跟条件运算符一起使用,用在extends关键字后面的父类型之中

概念有点难懂,这里举几个例子来说明下:

第一个例子我们看infer在数组上的应用

ts 复制代码
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

上面示例中,infer Item表示Item这个参数是 TypeScript 自己推断出来的,不用显式传入,而Flatten<Type>则表示Type这个类型参数是外部传入的。Type extends Array<infer Item>则表示,如果参数Type是一个数组,那么就将该数组的成员类型推断为Item并返回Item,即Item是从Type推断出来的

一旦使用Infer Item定义了Item,后面的代码就可以直接调用Item了。下面是上例的泛型Flatten<Type>的用法

typescript 复制代码
// string
type Str = Flatten<string[]>;

// number
type Num = Flatten<number>;

上面示例中,第一个例子Flatten<string[]>传入的类型参数是string[],可以推断出Item的类型是string,所以返回的是string。第二个例子Flatten<number>传入的类型参数是number,它不是数组,所以直接返回自身

第二个例子我们看 infer 在函数上面的应用

typescript 复制代码
type ReturnPromise<T> = T extends (...args: infer A) => infer R ? (...args: A) => Promise<R> : T;

上面示例中,如果T是函数,就返回这个函数的 Promise 版本,否则原样返回。infer A表示推断该函数的参数类型为Ainfer R表示推断该函数的返回值类型为R

第三个例子我们看 infer 在对象上面的应用

typescript 复制代码
type MyType<T> = T extends {
  a: infer M;
  b: infer N;
}
  ? [M, N]
  : never;

// 用法示例
type T = MyType<{ a: string; b: number }>;
// [string, number]

上面实例中,表示如果Tab两个属性,就把他们的类型MN提取出来

相关参考资料:

修饰符

  1. public:公共修饰符,可以被类的实例、子类和外部访问。默认情况下,类的成员都是公共的。
  2. private:私有修饰符,只能被类的内部访问。私有成员对于外部是不可见的,子类也无法访问。
  3. protected:受保护修饰符,可以被类的内部和子类访问,对于外部是不可见的。
  4. readonly:只读修饰符,表示成员只能在声明时或构造函数内部被赋值,之后不可修改。
  5. static:静态修饰符,用于定义类级别的成员,而不是实例级别的成员。静态成员可以通过类名直接访问,而不需要创建实例。
  6. abstract:抽象修饰符,用于声明抽象类和抽象方法。抽象类不能被实例化,只能被继承,并且子类必须实现抽象方法。

as const

as const是一种特殊的类型断言,它告诉Typescript编译器:

  1. 将这个值视为常量(const),禁止重新赋值
  2. 推导出最精准的字面量类型

第一个特性还比较容易理解:

ts 复制代码
let arr = [1, 2, 3, 4] as const;
arr = [1]; //Type '[1]' is not assignable to type 'readonly [1, 2, 3, 4]'.

as const断言过的变量是不能被更改的

第二个特性有点难理解,它的意思是as const会自动推导出最精准的字面量类型,而不是泛化的类型(如stringnumber[]),看下面这个例子:

ts 复制代码
let tuple = ["tesla", "model 3", "model X", "model Y"] as const;
let tuple2 = ["tesla", "model 3", "model X", "model Y"];
type tupleType = typeof tuple; // type tupleType = readonly ["tesla", "model 3", "model X", "model Y"]
type tuple2Type = typeof tuple2; // type tuple2Type = string[]
let arr: tupleType = ["tesla", 2, 3, 4];// error: Type 'number' is not assignable to type '"model 3"'.(2322)
let arr1: tupleType = ["tesla", "model 3", "model X", "model Y"];
let arr2: tuple2Type = ['str'];

我们定义了tupletuple2两个变量,其中tuple 加了 as const修饰符,使用typeof修饰符可以看到二者的不同:tuple的类型为readonly ["tesla", "model 3", "model X", "model Y"]tuple2的类型为string[]。所以使用tupleType时,必须为准确的字面量,而使用tuple2Type只要是字符串数组就可以。

T[number]

T[number] 用于获取数组/元组类型 T 的所有元素类型,返回一个联合类型。

具体使用可以看下面代码:

ts 复制代码
// 1. 基本数组类型
type Arr1 = number[];
type Elements1 = Arr1[number]; // number

// 2. 混合类型数组
type Arr2 = (string | number)[];
type Elements2 = Arr2[number]; // string | number

// 3. 元组类型
type Tuple1 = [string, number, boolean];
type Elements3 = Tuple1[number]; // string | number | boolean

// 4. 字面量元组
type Tuple2 = ["a", "b", "c"];
type Elements4 = Tuple2[number]; // "a" | "b" | "c"

// 5. 混合字面量元组
type Tuple3 = [1, "hello", true];
type Elements5 = Tuple3[number]; // 1 | "hello" | true

// 6. 空数组
type EmptyArr = [];
type Elements6 = EmptyArr[number]; // never

// 7. 只读数组
type ReadonlyArr = readonly [1, 2, 3];
type Elements7 = ReadonlyArr[number]; // 1 | 2 | 3
相关推荐
Fantastic_sj1 小时前
CSS-in-JS 动态主题切换与首屏渲染优化
前端·javascript·css
鹦鹉0071 小时前
SpringAOP实现
java·服务器·前端·spring
再学一点就睡5 小时前
手写 Promise 静态方法:从原理到实现
前端·javascript·面试
再学一点就睡5 小时前
前端必会:Promise 全解析,从原理到实战
前端·javascript·面试
前端工作日常6 小时前
我理解的eslint配置
前端·eslint
前端工作日常6 小时前
项目价值判断的核心标准
前端·程序员
90后的晨仔7 小时前
理解 Vue 的列表渲染:从传统 DOM 到响应式世界的演进
前端·vue.js
OEC小胖胖7 小时前
性能优化(一):时间分片(Time Slicing):让你的应用在高负载下“永不卡顿”的秘密
前端·javascript·性能优化·web
烛阴7 小时前
ABS - Rhomb
前端·webgl
植物系青年7 小时前
10+核心功能点!低代码平台实现不完全指南 🧭(下)
前端·低代码