重学TS --- 高级类型(1)

586 阅读5分钟

交叉类型(Intersection Types)

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

简单而言可以认为交叉类型即是所有类型的并集操作

例如, Person & Loggable同时是 Person Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员

interface Foo {
  name: string
}

interface Bar {
  age: number
}

const bar: Foo & Bar = {
  name: 'Klaus',
  age: 24
}

联合类型(Union Types)

联合类型与交叉类型很有关联,但是使用上却完全不同,联合类型可以看成是多个类型的交集

联合类型表示一个值可以是几种类型之一。 我们用竖线( |)分隔每个类型,所以 number | string | boolean表示一个值可以是 numberstring,或 boolean

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

// value的类型为number或者string
function getLength(value: number | string) {
  return typeof value === 'number' ?
    value.toString().length : value.length
}

console.log(getLength(123)) // => 3
console.log(getLength('Klaus')) // => 5
// console.log(getLength(true)) // error
interface Fish {
  swim: () => void,
  name: string
}

interface Dog {
  run: () => void,
  name: string
}

let animal: Fish | Dog = {
  swim() {
    console.log('swim')
  },
  run() {
    console.log('run')
  },
  name: 'animal'
}

// 联合类型只能访问联合类型上的公共属性或方法
console.log(animal.name)
// animal.swim() // error
// animal.run() // error

类型保护(Type Guards)

TS中为我们提供了联合类型,但是TS只会在编译阶段对我们的代码进行类型检测

所以我们在使用联合类型的时候,只能访问联合类型上边的公共属性和方法

如果我们需要使用联合类型上某一个具体类型,我们就不得不多次使用类型断言

为此TS提供了 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。

类型谓词 is

// type is number 就是类型谓词
// 当isString方法的返回值是true的时候 type的类型就是number
// 否则就是string
function isString(type: string | number): type is number {
  return typeof type === 'string'
}

typeof类型保护

function getLength(value: number | string) {
  // TypeScript会将变量类型从number|string缩减为number类型
  // 只要这个类型与变量的原始类型是兼容的。
  if (typeof value === 'number') {
    return value.toFixed()
  } else {
    return value.length
  }
}

使用typeof类型保护有两个先决条件

在表达式typeof v === "typename"

  1. 判断条件必须是全等,全不等,相等和不相等,不可以使用includes之类的其它判等条件
function getLength(value: number | string) {
  // (typeof value).includes('number')只会被TS认为是一条JS语句
  // 并不会产生任何的类型保护机制
  if ((typeof value).includes('number')) {
    return value.toFixed() // error
  } else {
    return value.length // error
  }
}
  1. typename的类型只能是"number""string""boolean""symbol"中的一种

    但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

instanceof类型保护

instanceof类型保护是通过构造函数来细化类型的一种方式

instanceof的右侧要求是一个构造函数

class User1 {
  constructor(public name: string) {
    this.name = name
  }
}

class User2 {
  constructor(public age: number) {
    this.age = age
  }
}

function getNameOrAge(user: User1 | User2) {
  if (user instanceof User1) {
    return user.name
  } else {
    return user.age
  }
}

console.log(getNameOrAge(new User1('Klaus')))
console.log(getNameOrAge(new User2(24)))

null 和 undefined

TypeScript具有两种特殊的类型, nullundefined

nullundefined可以看成是其它任意类型的子类型,可以赋值给其它任意类型

但是在默认情况下,TS会开启--strictNullChecks标记

此时 nullundefined不会作为其它任意类型的子类型,而是会作为单独类型进行使用

如果一个变量的确可以被赋值为nullundefined,那么需要使用联合类型

let str: string | undefined
str = 'Klaus'
str = undefined

使用了 --strictNullChecks,可选参数和可选属性会被自动地加上 | undefined

let user: {
  name?: number
} = {}

// 此时name的类型为 number | undefined
user.name = undefined // success
// 此时num2的类型就是number | undefined
function sum(num1: number, num2?: number) {}

sum(1, undefined) // success

非空断言

由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null

function fun(param: string | null): string {
  return param || "default";
}

如果编译器不能够去除 nullundefined, 我们可以使用非空断言, 语法是添加 !后缀

例如: identifier!identifier的类型里去除了 nullundefined

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    // name一定不为null
    // charAt(0) 取出name的第一个字符
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

类型别名

类型别名会给一个类型起个新名字。

起别名不会新建一个类型,它创建了一个新 名字来引用那个类型。

// 使用type关键字定义类型别名
type Foo = () => void

const foo: Foo = () => { console.log('foo') }
foo()

同接口一样,类型别名也可以是泛型 (在别名声明的右侧传入),可以使用类型别名来在属性里引用自己

type User<T> = {
  name: T,
  friend?: User<T>
} // success

// 类型别名不能出现在声明右侧的任何地方, 只能在别名声明的后边
type arr = Array<T> // error
type LinkedList<T> = T & { next?: LinkedList<T> };

interface Person {
    name: string;
}

let people: LinkedList<Person> = {
  name: 'Klaus',
};

let s: string | undefined = people.name;
s = people.next?.name;
s = people.next?.next?.name;
s = people.next?.next?.next?.name;

接口 和 类型别名的区别:

  1. 接口创建了一个新的数据类型,可以在其它任何地方使用。 类型别名并不创建新的数据类型。
  2. 类型别名不能被 extendsimplements(自己也不能 extendsimplements其它类型)