typescript(7)- 高级类型 | 青训营笔记

180 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的第14天

「前言」

之前的文章关于类型的讲解,主要围绕着 ts 提供的基本类型,本文在这基础之上,全面的展开对 ts 类型 这一问题的深入

「类型组合」

交叉类型

交叉类型是将多个类型合并为一个类型。用 & 连接多个类型,与数学上面的 并集 同理。

interface I1 {
  id: string;
  show1: () => void;
  name: string;
}

interface I2 {
  show2: () => void
}

const obj: I1 & I2 = {
  id: '01',
  name: '张三',
  show1() { },
  show2() { }
}

这两个接口中的属性和方法在变量必须全部满足

  • 如果交叉类型中的类型成员有相同的属性名,类型不同,如果至少有一个是多个简单类型,例如 number & string,最终会被视作 never 类型
  • 如果交叉类型中的类型成员有相同的方法名,类型不同
    • 返回值类型不同,void 类型会被其他类型覆盖,多个非 void 类型怎么继续按照交叉类型生成最终的返回值类型,参考上一条规则
    • 参数列表不同,不论是参数列表的个数还是参数类型差异都会被忽略掉

联合类型

联合类型表示一个值可以是几种类型之一,与交叉类型相对,使用 | 符号连接多个类型,表示 交集 的关系

interface I1 {
  id: string;
  show1: () => void;
  name: string;
}

interface I2 {
  show2: () => void
}

const obj: I1 | I2 = {
  id: '01',
  name: '张三',
  show1() { },
  show2() { }
}

如果在声明变量的时候 初始化值的成员,则表现和 联合类型 无差别,因为 ts 在这个时候不知道你所写的内容具体是什么类型的,只能兼容所有类型

正确的打开方式

const o: I2 = {
  show2() { }
}

const obj: I1 | I2 = o;

「类型保护」

在之前的文章中有这么一个例子

场景:定义一个函数获取数字类型或者字符类型的长度

我们起初使用 typeof类型断言 来解决的

function getLen(x: number | string) {
  if (typeof x === 'number') {
    return x.toString().length;
  } else {
    return x.length;
  }
}
function getLen(x: number | string) {
  if ((<string>x).length) {
    return (x as string).length; // 这一行和上一行都是类型断言的写法
  } else {
    return x.toString().length;
  }
}

第一种方式其实展示了 ts 的一种语法---类型保护,一旦我们使用这种类型保护,在之后的分支中就能明确知道类型了,然而使用 类型断言 的方式,在分支中的语句不得不多次使用类型断言约束类型。

能像 typeof 这种方式触发类型保护的方式有以下几种

类型谓词

我们只要简单地定义一个函数,它的返回值是一个 类型谓词,实现类型保护

function getLen(x: number | string) {
  if (isNumber(x)) {
    return x.toString().length;
  } else {
    return x.length;
  }
}

function isNumber(x: number | string): x is number {
  return true;
  //  或者 return typeof x === 'number';  
}

在这个例子里, x is number就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前参数列表里的一个参数名。函数 返回值 必须为 布尔值

typeof

typeof类型保护只有两种形式能被识别: typeof v === "typename"和 typeof v !== "typename", "typename"必须是 "number", "string", "boolean"或 "symbol"
如果是其他类型,会被推断为 never 类型

instanceof

instanceof类型保护 是通过构造函数来细化类型的一种方式。
typeof 用法一致,x instanceof ConstructorConstructor 为构造函数

更多的例子

场景:数组中的每个成员为不同类型,但有相似的属性

type IBookItem
  = { author: string }
  & ({
    type: 'computer';
    range: string;
  }
    | {
      type: 'history';
      theme: string;
    })

const bookList: Array<IBookItem> = [
  {
    author: '罗贯中',
    type: 'history',
    theme: '三国演义'
  },
  {
    author: 'ts',
    type: 'computer',
    range: '2001-2022',
  }
]

这里我们使用了 交叉类型联合类型,简化了对类型的约束,但这不是重点,看下面的例子

function logBook(book: IBookItem) {
  console.log(book.author);
  if (book.type === 'history') {
    console.log(book.theme);
  } else {
    console.log(book.range);
  }
}

这里的也实现了 类型保护,在判断分支的外部,我们只能访问得到 联合类型 的公共属性,在内部,经过更细致的判断,可以直接访问对应的成员了,同时也增强了 ide,拥有正确的代码自动补全

交叉类型 + 类型保护 = 自动类型推断

「关于 null」

TypeScript具有两种特殊的类型, null和 undefined,它们分别具有值null和undefined。
默认情况下,类型检查器认为 null与 undefined可以赋值给任何类型。

如果你想阻止这条规则,可以在 tsconfig.json 文件中设置 "strictNullChecks": true ,当你声明一个变量时,它不会自动地包含 null或 undefined。 你可以使用联合类型明确的包含它们

下面的代码默认你开启了 "strictNullChecks": true

带来的变化

  • 可选参数和可选属性会被自动地加上 | undefined
function test(num?: number) { }

test(1);
test(undefined);
test(null); // error,类型“null”的参数不能赋给类型“number | undefined”的参数。
interface I {
  num?: number;
}

const obj = <I>{};
obj.num = undefined;
obj.num = null; // 不能将类型“null”分配给类型“number | undefined”。

解决办法

  1. 可以在类型注解的后面手动添加 | null
  2. 如果要使用带有 null 类型,可以使用类型保护方式的限制
  3. 通过 ! 后缀去除掉 null和 undefined 类型
interface I {
  num?: number | null;
}

const obj = <I>{};
obj.num = undefined;
obj.num.toFixed(); // error
obj.num || obj.num!.toFixed(1);

「类型别名」

类型别名会给一个类型起个新名字。我们使用 type 关键字来声明一个新的类型,在之前的文章有提及过 type 简单的使用方法,这里我们讨论更高级的用法

带有泛型的类型别名

二叉树节点类型的定义

type Tree<T> = {
  value: T;
  left: Tree<T>;
  right: Tree<T>;
}

我们也可以使用类型别名来在属性里引用自己

type Tree<T> = T & {
  left: Tree<T>;
  right: Tree<T>;
}

interface INode {
  value: number;
}

const tree: Tree<INode> = {
  value: 1,
  left: null,
  right: null
}

与 interface 的区别

尽管 type 可以像 interface 定义诸多类型,但是他们之间也有细微的差别

最大的区别type 不能使用 extendsimplements,但是 type 可以被 extendsimplements

type INum = {
  num: number;
}

interface I extends INum { }

「索引类型」

keyof 关键字

索引类型查询操作符 keyof。 对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合

class P {
  private name = 'zd';
}

function test<T, K extends keyof T>(o: T, key: K): T[K] {
  return o[key];
}

test(new P(), 'name') // error

索引类型和字符串索引签名

interface IObject<T> {
  [keys: string]: T;
}

let keys: keyof IObject<number>; // keys 为 string | number 类型
let values: IObject<number>['foo']; // values 为 number 类型 

映射类型

TypeScript 提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。

例如,你可以令每个属性成为 readonly类型或可选的。

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
}
type Partial<T> = {
  [P in keyof T]?: T[P];
}

我们使用 in 关键字遍历到 keyof T 中的内容,作用类似于 vuev-for="item in data"

注意:这里使用的类型被包含进了 TypeScript 的标准库

更复杂的例子(vue 中 reactive 的类型):

type Proxy<T> = {
  get(): T;
  set(value: T): void;
}

type Reactive<T> = {
  [P in keyof T]: Proxy<T[P]>;
}

function reactive<T>(o: T): Reactive<T> {
  // ... wrap proxies ...
  return;
}
let proxyProps = reactive({});