TypeScript - 语法细节

135 阅读5分钟

TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型

联合类型

使用 或运算符 来讲多种类型组合 形成的类型 叫做 联合类型(Union Type)

  • 联合类型是由两个或者多个其他类型组成的类型
  • 表示可以是这些类型中的任何一个值
  • 联合类型中的每一个类型被称之为联合成员(union's members)
function printId(id: number | string) {
  console.log(id)
  
  // 如果我们需要使用联合类型,需要使用类型缩小将联合类型缩小为某一个具体的类型,从而保证js代码的健壮性
  if (typeof id === 'string') {
    console.log(id.length)
  }
}

交叉类型

联合类型表示多个类型中一个即可,而交叉类似表示需要满足多个类型的条件

交叉类型使用 & 符号进行连接

// 需要一个类型 即满足 是 number 又需要是string
// 不存在这样的类型 所以newType的类型是 never
type newType = number & string

所以实际开发中,主要是在对象类型上使用交叉类型

interface IName {
  name: string
}

interface IAge {
  age: number
}

const user: IName & IAge = {
  name: 'Klaus',
  age: 23
}

类型别名

我们通过在类型注解中编写 对象类型 和 联合类型,但是当我们想要多次在其他地方使用时,就要编写多次

所以我们可以给对应的类型起一个别名,以便于多次调用

type Point = { 
  // 字段和字段之间使用分号或者逗号或者换行进行分割
  x: number
  y: number
}

接口

我们除了可以通过类型别名定义一个新的类型,也可以通过接口定义一个新的类型

interface Point {
  x: number
  y: number
}

接口和类型别名的区别

类型别名和接口非常相似,在定义对象类型时,大部分时候,你可以任意选择使用

// 定义类型别名是通过 赋值的方式进行定义的
type Point = {
  x: number
  y: number
  // 可选类型
  z?: number
}

// 定义接口 是通过 类型声明的方式进行定义的
interface IPoint {
  x: number
  y: number
  z?: number
}
// 类型别名可以为非对象类型起别名,也可以给对象类型起别名
// 但是接口 只能给对象类型起别名
type NewType = number | string // => 联合类型不是对象类型,所以只能使用类型别名

interface 可以重复定义某个接口中的属性和方法,多个同名接口会合并成一个接口

type的作用是对类型起别名,别名是不能重复的

// error -> Point不能重复定义
type Point {
  x: number
  y: number
}

type Point {
  z: number
}
interface Point {
  x: number
  y: number
}

interface Point {
  z: number
}

const point: Point = {
  x: 100,
  y: 100,
  z: 100
}

接口支持继承,类型别名不可以

// 接口命名 一般使用  I + 大驼峰
interface IPerson {
  name: string
  age: number
}

interface Student extends IPerson {
  sno: string
}

const stu: Student = {
  name: 'Klaus',
  age: 23,
  sno: '1800166'
}

如果是用来定义对象类型,那么推荐使用接口来进行定义

但是如果是用来定义其它数据类型的类型,推荐使用type来进行定义

类型断言

有时候TypeScript无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions)

TypeScript只允许类型断言转换为 更具体 或者 不太具体 的类型版本,此规则可防止不可能的强制转换

类型断言有两种书写方式:

  1. xxxx as string --- 推荐的写法
  2. <string> xxxx --- 不推荐 因为在在JSX中不支持这种写法,会被识别为标签
// 对于document.getElementById 获取到的结果只能是 HTMLElement | undefined
// 所以此时imgEl上不能使用src, alt 这类的img元素特有的属性,此时可以使用类型断言
const imgEl = document.getElementById('avatar') as HTMLImageElement | undefined

if (imgEl) {
  imgEl.src = ''
  imgEl.alt = ''
}

非空类型断言 !

非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它是否为空的检测

const imgEl = document.getElementById('avatar') as HTMLImageElement | undefined

// 非空类型断言 本质上是不安全的 所以需要谨慎使用
imgEl!.src = ''
imgEl!.alt = ''

字面量类型

单独使用字面量类型是没有太大的意义的,但是我们可以将多个类型联合在一起

type Direction = 'left' | 'right' | 'up' | 'down'

字面量推理

const user = {
  name: 'Klaus',
  age: 18
}

/* 
  const user: {
    name: string
    age: number
  }
*/
// as const 可以将 更为宽泛的类型 转换为 更为具体的字面量类型
// 对一个对象类型进行字面量推理的时候,对应的对象类型的属性类型都是只读的
const user = {
  name: 'Klaus',
  age: 18
} as const

/*
  const user: {
    readonly name: "Klaus";
    readonly age: 18;
  }
*/

类型缩小

类型缩小的英文是 Type Narrowing(也有人翻译成类型收窄)

我们可以通过类似于 typeof padding === "number" 的判断语句,来改变TypeScript的执行路径

在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为 缩小( Narrowing )

而我们编写的 typeof padding === "number 也可以称之为 类型保护(type guards)

常见的类型保护方式

typeof

function printID(id: number | string) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase())
  }
}

平等缩小

可以使用Switch或者相等的一些运算符来表达相等性(比如===, !==, ==, and != )

type Direction = 'left' | 'right' 

function checkDirection(direction: Direction) {
  if (direction === 'left') {
    console.log('left')
  } else {
    console.log('right')
  }
}

instanceof

// 实例对象的类型就是其对应的类
function getDate(date: string | Date) {
  if (date instanceof Date) {
    console.log(date.toISOString())
  }
}

in操作符

Javascript 有一个运算符,用于确定对象是否具有带名称的属性:in运算符

如果指定的属性在指定的对象或其原型链中,则in 运算符返回true

interface ISwim {
  swim: () => void
}

interface IRun {
  run: () => void
}

const fish: ISwim = {
  swim() {}
}

const cat: IRun = {
  run() {}
}

function checkAnimal(animal: ISwim | IRun) {
  if ('swim' in animal) {
    animal.swim()
  } else {
    animal.run()
  }
}