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 类型设计。

本文参考

相关推荐
活宝小娜2 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点2 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow2 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o2 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic3 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā3 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年4 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder4 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_882727575 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
SoaringHeart5 小时前
Flutter进阶:基于 MLKit 的 OCR 文字识别
前端·flutter