TypeScript-类型与类型体操

本文使用「署名 4.0 国际 (CC BY 4.0)」 许可协议,欢迎转载、或重新修改使用,但需要注明来源。

TypeScript 的类型

1. TypeScript 类型是什么?可以参考下图

2. 集合之间的普遍操作

  • 两个集合的相交部分,在 TypeScript 中怎么表示

  • 两个集合的合并起来,在 TypeScript 中怎么表示

  • 两个不相交的集合的相交部分是什么,在 TypeScript 中怎么表示

  • 如何判断一个类型是否在另一个类型范围里

3. 类型范围的概念

  • 对于下图,可以明显看出类型之间的范围和包含关系,Point1D 范围最大,Point1D 类型包含 Point2D,Point2D 为 Point1D 的子类型。 我们可以判断 Point2D extends Point1D 为 true 。extends 即表示 Point2D 为 Point1D 本身或者其范围内更小的一个范围代表的类型。
  • 对于下图中的交叉类型 Point & Named 拥有键 x,y,name 在 Point 类型上增加了 键 name。所以该交叉类型更精确,范围更小。同理 Named 类型也是。
  • 对象类型范围和你想的不一样,其实看上图就知道了。对于空对象类型 {},它和那些 string number 类型的字面量类型不一样。空对象类型范围其实很大。包含所有可能的键与值的对象类型情况。总之越精确范围越小。

4. 类型之间的关系

类型之间的关系不仅是关系。最终还是会反映到类型范围上。比如以下例子。

typescript 复制代码
interface Point1D {
  x: number;
}
interface Point2D extends Point1D {
  y: number;
}
interface Point3D extends Point2D {
  z: number;
}

5. 常见 TypeScript 报错 xxxis not assignable to 一般原理

  • 是因为两个类型范围之间有出入,正确的情况是值的类型是被赋值的那个类型的本身或者被赋值的类型的子类型。所以错误的情况,有的是一个更大更宽泛的类型赋值给了一个小的类型,就类似于两个圆,一个范围更大的圆包着小的圆,这种情况一般是对自己写的那部分进行类型收窄,或者断言成那个更小的类型。有的是两个类型之间无任何重叠部分的类型,类似于两个不相交的圆,这时候要对写的这部分类型需要重新改动,或者双重断言了。

类型体操的知识点与技巧

空 TypeScript playground 供使用 可以打开该链接复制后边代码看效果

1. T,K,V 这些泛型的一般意义

T,U 一般都是指的类型参数。T Type. U 在字母表上紧接着 T。K(Key) 一般指的是对象类型中的键。V(Value)一般指的是对象中的值。

2. never 表示什么都没有,是类型系统中范围最小的类型

3. 索引访问类型(indexed access type)

typescript 复制代码
   type User = {
     id: number;
     name: string;
   }
   type UserKeys = keyof User; // "id" | "name"
   type U1 = User["id"] // number
   type U2 = User["id" | "name"] // string | number   // 可以用联合类型作为索引
   type U3 = User[keyof User] // string | number

   const arr = ['super','great','splendid'] as const
   type ArrType = typeof arr // readonly ["super", "great", "splendid"]
   type DescribeWords = ArrType[number] // "super" | "great" | "splendid".  这也是索引访问
   // 可以简写为   
   type DescibeWords = typeof arr[number] // "super" | "great" | "splendid"

4. 条件类型(Conditional Types)

条件类型是 TypeScript 类型是 TypeScript 类型运算最基本的语法。因为与类型集合有关的逻辑基本都需要 extends 来表示。 Conditional Types ------------ T extends U ? X : Y

typescript 复制代码
type IsString<T> = T extends string ? true : false;

type I0 = IsString<number>;  // false
type I1 = IsString<"abc">;  // true
type I2 = IsString<any>;  // boolean 为什么是boolean。之后会讲,涉及到distribute。 
                           // any是包括所有类型,是范围最大的类型

5. 条件类型的分发 (distribute)

Exclude 大家肯定了解。但看它的实现,乍眼看返回的类型是 never,咋会返回除去指定类型的联合类型呢?

typescript 复制代码
type T0 = Exclude<"a" | "b" | "c", "a">
// type T0 = "b"|"c"
type Exclude<T, U> = T extends U ? never : T

其实这里是有条件类型的分发

typescript 复制代码
T extends U ? X : Y 
这个 T 是判断是否是 distributed conditional type的参数
不被 T[] [T] Promise<T> 所包裹

分发条件: 在条件类型中,如果 T 不被数组,元组和 Promise 包裹,就会触发分发。联合类型会分成几个分支判断。

过程:

typescript 复制代码
T extends U ? X : Y 
T => A | B | C
A | B | C extends U ? X : Y  =>
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
typescript 复制代码
// 触发 条件类型分发的例子:
type Naked<T> = T extends boolean ? "Y" : "N";
type T0 = Naked<number | boolean>; // "Y"|"N"

// 未触发 条件类型分发的例子:
type WrappedTuple<T> = [T] extends [boolean] ? "Y" : "N";
type WrappedArray<T> = T[] extends boolean[] ? "Y" : "N";
type WrappedPromise<T> = Promise<T> extends Promise<boolean> ? "Y" : "N";

type T1 = WrappedTuple<number | boolean>; // "N"
type T2 = WrappedArray<number | boolean>; // "N"
type T3 = WrappedPromise<number | boolean>; // "N"

所以 Exclude 的例子:

typescript 复制代码
type Exclude<T, U> = T extends U ? never : T;
type T4 = Exclude<"a" | "b" | "c", "a" | "b">

("a" extends "a" | "b" ? never : "a") // => never
| ("b" extends "a" | "b" ? never : "b") // => never
| ("c" extends "a" | "b" ? never : "c") // => "c"

never | never | "c" // => "c"

6. 映射类型(Mapped Types)

大家肯定看过 Partial、Required 的实现。

typescript 复制代码
type Partial<T> = {
    [P in keyof T]?: T[P]
}

在映射类型中我们可以再映射(remapping keys),语法如下:

typescript 复制代码
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]  // 这里的as就是key的再映射
  };              // 用到 & 是因为对象类型中K能为number Symbol string。此处为确保为string类型
 interface Person {
    name: string;
    age: number;
    location: string;
}
type LazyPerson = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getLocation: () => string;
// }

除此之外,在再映射时,我们可以通过返回 never 过滤 key。例子如下:

typescript 复制代码
// Exclude
type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
    kind: "circle";
    radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
//   type KindlessCircle = {
//       radius: number;
//   };

7. infer

以下场景,怎么解决?

typescript 复制代码
// 如何获取 Promise内的类型
type X = Promise<string>;
type Y = Promise<{ field: number }>;
type ResultX = Transform<X>; // string
ResultY = Transform<Y>; // { field: number }
Transform<A> = ??

// 如何获取数组的类型
type T0 = string[];
type U0 = UnpackedArray<T0> // string
type UnpackedArray<T> = ??

// 除了用 ReturnType如何用其他方法拿到函数的返回类型呢
type T1 = () => string;
type U1 = UnpackedFn<T1>; // string
type UnpackedFn<T> = ??

答案:用 infer ! infer 是用来声明一个在模式匹配过程中匹配的可以存起来的类型变量

typescript 复制代码
type Transform<A> = A extends Promise<infer Inner> ? Inner : never
type UnpackedArray<T> = T extends (infer U)[] ? U : T
type UnpackedFn<T> = T extends (...args: any[]) => infer U ? U : T;

值得注意的是,infer 只能用在条件语句的 extends 后,且只能在条件类型的 true 分支。

类型体操的练习

练习题集及答案

TypeScript playground 练习题集

注意以下内容全是 类型!!不是值

类型范围及概念

1. 解决报错

typescript 复制代码
type User = {
  id: number;
  kind: string;
};

function makeCustomer<T extends User>(u: T): T {
  // Type '{ id: number; kind: string; }' is not assignable to type 'T'.
  // '{ id: number; kind: string; }' is assignable to the constraint of type 'T', 
  // but 'T' could be instantiated with a different subtype of constraint 'User'.
  return {
    id: u.id,
    kind: 'customer'
  }
}

2. 根据集合之间的关系,实现一个 IsEqual 工具类型,用于比较两个类型是否相等。具体的使用示例如下所示:

typescript 复制代码
type IsEqual<A, B> = // 你的实现代码

// 测试用例
type E0 = IsEqual<1, 2>; // false
type E1 = IsEqual<{ a: 1 }, { a: 1 }> // true
type E2 = IsEqual<[1], []>; // false

索引访问类型

  1. 实现一个 OptionalKeys 工具类型,用来获取对象类型中声明的可选属性。具体的使用示例如下所示:
typescript 复制代码
type Person = {
  id: string;
  name: string;
  age: number;
  from?: string;
  speak?: string;
};

type OptionalKeys<T> = // 你的实现代码
type PersonOptionalKeys = OptionalKeys<Person> // "from" | "speak"

映射类型

  1. 定义一个 SetOptional 工具类型,支持把给定的 keys 对应的属性变成可选,对应的使用示例如下所示:
typescript 复制代码
type Foo = {
        a: number;
        b?: string;
        c: boolean;
}

// 测试用例
type SomeOptional = SetOptional<Foo, 'a' | 'b'>;

// type SomeOptional = {
//         a?: number; // 该属性已变成可选的
//         b?: string; // 保持不变
//         c: boolean; 
// }
  1. 实现一个 Merge 工具类型,用于把两个类型合并成一个新的类型。第二种类型(SecondType)的 Keys 将会覆盖第一种类型(FirstType)的 Keys。具体的使用示例如下所示:
typescript 复制代码
type Foo = {
    a: number;
    b: string;
};

type Bar = {
    b: number;
};

type Merge<FirstType, SecondType> = // 你的实现代码

const ab: Merge<Foo, Bar> = { a: 1, b: 2 };

infer 使用

  1. 实现一个 Reverse 工具类型,用于对元组类型中元素的位置颠倒,并返回该数组。元组的第一个元素会变成最后一个,最后一个元素变成第一个。
typescript 复制代码
type Reverse<
  T extends Array<any>,
  R extends Array<any> = []
> = // 你的实现代码

type R0 = Reverse<[]> // []
type R1 = Reverse<[1, 2, 3]> // [3, 2, 1]

关于类型体操个人观点

做这些类型体操的题,确实能提高对 TypeScript 语法的熟悉度和构造复杂类型工具的能力,但也不用过分追求这个,个人认为在个人日常业务代码中,写出拥有完善规范的 TypeScript 类型规定,和类型系统设计的代码,这比类型体操重要得多。至于完善规范的 TypeScript 类型规定,少写 anyscript,多看多模仿代码写的规范的同事的代码,多看开源库比如 ramda.js 的源码,看看它们的 TypeScript 类型设计。

本文参考

相关推荐
Nan_Shu_6141 分钟前
学习: Threejs (2)
前端·javascript·学习
G_G#9 分钟前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界25 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路34 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug37 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213839 分钟前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中1 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子2 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端