TS从入门到放弃【十二】:高级类型(一)

208 阅读4分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。

有了前面所学内容的铺垫,本章我们学习的高级类型中,有一些是之前所学知识的结合,有一些是在之前内容上提升的新东西。

学习高级类型可以满足我们工作中更多场景的需求。

1、交叉类型

交叉类型就是取多个类型的并集,使用 & 符号定义。

const mergeFunc = <T, U>(arg1: T, arr2: U): T & U => {
  let res = {} as T & U; // 使用类型断言来告诉TS这里是(T和U)的交叉类型
  res = Object.assign(arg1, arr2);
  return res;
};
mergeFunc({ a: 'a' }, { b: 'b' });

2、联合类型

例如:type1 | type2 | type3,这里是三种类型组成的联合类型,用到联合类型的地方,只要类型是这三种类型其中的一种就可以了。

来看下面的例子:

const getLength = (content: string | number): number => {
  if(typeof content === 'string') {
    return content.length;
  } else {
    return content.toString().length;
  }
}

3、类型保护

我们知道 TS 在编译阶段就会为我们检查到一些错误,但是有一些值是在代码运行起来之后才能确定的。

const valueList = [123, 'abc'];
// 这个函数会随机从 valueList 取值
const getRandomValue = () => {
  const number = Math.random() * 10;
  if(number < 5) return valueList[0];
  return valueList[1];
};
const item = getRandomValue();

此时 item 在编译阶段是无法知道它的类型的。所以,我们有时候就需要先判断它的类型。

if(item.length) {
  console.log(item.length);
} else {
  console.log(item.toFixed());
}

上述代码这段逻辑在 JS 中是没有问题的,但是在 TS 中就会报错:类型“string | number”上不存在属性“length”

类型断言

if((item as string).length) {
  console.log((item as string).length);
} else {
  console.log((item as number).toFixed());
}

但是类型断言有个缺点:如果多个地方使用到 item,每个使用到的地方都需要类型断言

typeof 类型保护

我们先来自定义类型保护(它是一个方法)

function isString(value: number | string): value is string {
  return typeof value === 'string';
}

返回类型是 value is string,表示判断 是否是 string 类型。

现在我们来使用类型保护

if(isString(item)) {
  console.log(item.length);
} else {
  console.log(item.toFixed());
}

可以看到,这里我们多定义了一个函数 isString,实际上定义函数的方式多用在复杂的类型保护。类型保护如果像这里一样,比较简单的话,建议直接使用 typeof:(实际上就是把函数拆出来)

if(typeof value === 'string') {
  console.log(item.length);
} else {
  console.log(item.toFixed());
}

在 TS 中,typeof类型保护,只能用等号或者不等来比较,并且只能比较简单类型:string、number、boolean、symbol

注:typeof在ts中可以判断很多种类型,但是如果用作类型保护的话,只能是这四种:string、number、boolean、symbol

instanceof 类型保护

instanceof 用来判断一个实例是否是某个构造函数(或者某个class)创建的。

在 TS 中 instanceof 同样具有类型保护的效果。

class CreatedByClass1 {
  public age = 18;
  constructor() {}
}
class CreatedByClass2 {
  public name = 'dylan';
  constructor() {}
}
function getRandomItem() {
  return Math.random() < 0.5 ? new CreatedByClass1() : new CreatedByClass2();
}
const item = getRandomItem();
if(item instanceof CreatedByClass1) {
  console.log(item.age);
} else {
  console.log(item.name);
}

4、null 和 undefined

注意需要将tslint的 strictNullChecks 设置为 false。如果设置为 true的话,可选参数会被自动加上 undefined 变为联合类型

// 这里 y 会发现它是一个 number | undefined 联合类型
const sumFunc = (x: number, y?: number) => {
 	return x + (y || 0) 
}; 

在 TS 中 string | undefinedstring | nullstring | undefined | null 这三个是三种完全不同的类型

5、类型断言

在有些情况下,编译器是无法在我们声明一些变量前知道它的值是否为 null,所以我们需要断言,主动指名变量的值不为 null

function getSplicedStr(num: number | null): string {
  function getRes(prefix: string) {
    // 当我们设置 strictNullChecks 为 true 时,这里 num 会报错:对象可能为 "null"。
    return prefix + num.toFixed().toString();
  }
  num = num || 0.1;
  return getRes('dylan-');
}

我们可以看到,num 此时类型为:(parameter) num: number | null

在这个例子中,因为有嵌套函数,编译器是无法去除嵌套函数的 null 的,所以这里可以使用类型断言 !

function getSplicedStr(num: number | null): string {
  function getRes(prefix: string) {
    // 使用 ! 进行类型断言,主动告诉编译器这里一定有值
    return prefix + num!.toFixed().toString();
  }
  num = num || 0.1;
  return getRes('dylan-');
}

6、类型别名

type 关键字来定义类型别名。不是创建了一个新的对象,而是通过引用来使用这个对象。

type TypeString = string;
let str: TypeString;

类型别名也可以使用泛型

type PositionType<T> = { x: T, y: T }
const position1: PositionType<number> = {
  x: 1,
  y: -1,
}
const position2: PositionType<string> = {
  x: 'left',
  y: 'top',
}

使用类型别名的时候,也可以在属性中引用自己:

type Childs<T> = {
  current: T,
  child?: Childs<T>, // 树状结构
};

类型别名只是为其他类型起了个新的名字来引用这个类型,所以当它为接口起别名时,不能使用 extendsimplements。因为它只是个名字,不是一个真正的接口。

type Alias = {
  num: number
}
interface InterFace {
  num: number
}
let _alias: Alias = {
  num: 123
}
let _interface: InterFace = {
  num: 321
}

可以看到,类型别名和接口都可以定义一个只包含一个参数为 num的值。并且他们的类型是兼容的

_alias = _interface; // OK

那么,我们什么时候用类型别名,什么时候用接口呢?

  • 接口:当你定义的类型,需要用于扩展(extends、implements)的时候
  • 类型别名:当无法通过接口并且需要使用联合类型元组类型的时候

7、字面量类型

字面量类型不适合放在基础类型中来讲,因为字符串字面量类型其实和字符串类型并不一样。

字面量类型包含两种,分别是:数字字面量、字符串字面量。

type Name = 'Dylan'; // 字符串字面量类型
const name: Name = 'Dylan1'; // Error:不能将类型“"Dylan1"”分配给类型“"Dylan"”。

使用联合类型来定义多个字符串字面量

type Direction = 'north' | 'east' | 'south' | 'west';
function getDirection(direction: Direction) {
  return direction;
}
getDirection("east"); // 这里传入参数就会有提示,只能选这四个

数字字面量类型

type Age = 18;
interface InfoInterface {
  name: string;
  age: Age
}
const _info: InfoInterface = {
  name: 'dylan',
  age: 18, // 这里的 age 只能填 18
}

8、可辨识联合

可辨识联合要求具有两个要素:

  1. 具有普通的单例类型属性(作为辨识的特征)
  2. 一个类型别名包含了哪些类型的联合(把几个类型封装成联合类型,并且起个别名)
interface Square {
  kind: 'square';
  size: number;
}
interface Rectangle {
  kind: 'reactangle';
  height: number;
  width: number;
}
interface Circle {
  kind: 'circle';
  radius: number;
}

上面代码我们定义了三个接口,他们都有同一个特征 kind,值都不一样

接下来我们再定义一个联合类型:

type Shape = Square | Rectangle | Circle;

再来,我们定义一个函数:

function assertNever(value: never): never {
  throw new Error('Unexpected object: ' + value);
}
function getArea(s: Shape): number {
  switch(s.kind) {
    case "square": return s.size * s.size;
    case "reactangle": return s.height * s.width;
    case "circle": return Math.PI * s.radius ** 2;
    default: return assertNever(s); // !完整性检查
  }
}