TS 中的类型

288 阅读9分钟

之前通过《TS 的安装与编译》介绍了我们如何在自己的电脑编写 ts 文件并把它们编译为 js。今天我们继续介绍些关于 ts 的知识,既然 ts 最主要的作用就是来给 js 做类型检测的,那么本文就来介绍下 ts 中的那些类型吧~

基本数据类型

TypeScript 是 JavaScript 的 超集,所以 ts 中也有字符串数字布尔值undefinednull 这些基本数据类型,没啥好说的,但要注意两点:

  • 类型注解(Type Annotation)的类型都是小写

比如 const name: string = 'hello',如果是大写的 Stringconst name: String = 'hello',也不会报错,但表示的是 js 中的 String 类,只不过可以赋值为一个字符串而已。

  • undefinednull 是所有类型的子类型:

比如下面这样的写法是不会报错的(tsconfig.json 里关闭严格模式,"strict": false

let str: string = 'a'
str = 'b'
str = undefined
str = null

但如果变量本身的类型就是 undefinednull,那么其值只能是 undefinednull,也就是说它们既是值,也是类型:

const u: undefined = undefined
const n: null = null

any(任意类型)

一个变量设置类型为 any 后相当于对该变量关闭了 ts 的类型检测,它可以赋值为任意值,并且可以进行任意的操作(比如去获取并不存在的属性),一般不建议使用 any 类型。any 类型的值可以赋值给任意变量:

let a: string = 'a', b: any = 10
a = b
console.log(a) // 10

声明变量(但不赋值)如果不指定类型,则 ts 解析器会自动判断变量类型为 any(隐式的 any);但是如果在声明变量的时候赋值了,则 ts 会将变量推断为对应的类型。

unknown

表示未知类型的值,相当于类型安全的 any,但是在 unknown 类型的变量上进行任意的操作都是不合法的:

image.png

unknown 类型的变量,不可以直接赋值给 unknownany 类型之外的变量:

let e: unknown
e = 1

let s: string
s = e // 报错 不能将类型“unknown”分配给类型“string”

如果想让 unknown 类型的值赋值给其他类型的值,或者想对 unknown 类型的值做一些操作,有 2 种方法:

类型缩小

let e: unknown
e = 1

let s: string
if (typeof e === 'string') {
  console.log(e.length)
  s = e 
}

if 判断起到了类型缩小(Narrowing)的作用,typeof e === 'string' 可以称为类型保护(type guards)。除了 typeof,类型保护还可以根据不同场景使用 instanceof===in 等,起到类型缩小的目的。

类型断言

通过使用类型断言来告诉解析器变量的实际类型

  • 写法一:变量 as 类型
let e: unknown
e = 'hello'

let s: string
s = e as string
console.log((e as string).length)
  • 写法二:<类型>变量
let e: unknown
e = 'hello'

let s: string
s = <string>e
console.log((<string>e).length)

强制类型转换

我们还能通过类型断言来强制进行一些赋值。比如有个类型为 stringname

const name = 'Jay'

想把它赋值给类型为 numberage,肯定会报错:

image.png

就可以通过类型断言将 name 断言为 number,但是不能直接 name as number

image.png

这是因为 ts 只允许类型断言转换为更具体或更宽泛的类型,以防止不可能的强制转换,如果一定要强制转换,需要将 name 先转为 anyunknown

const age: number = (name as unknown) as number

数组

ts 中定义数组有两种方式:

  1. 方式一: let 变量名: 数据类型[] = ['元素 1', '元素 2'],数组里元素都只能为定义的类型,如果有不同类型会报错。
let arr: number[] = [1, 2, 3]
  1. 方式二:使用数组泛型(相当于是类使用了泛型),let 变量名: Array<数据类型> = ['元素 1', '元素 2'],注意这里 ArrayA 是大写:
let arr: Array<number> = [1, 2, 3]

推荐使用方式一,因为方式二可能会与 react 或 vue 的 jsx 冲突。

元组(Tuple)

因为 ts 中数组里的元素最好都是同种类型的,但有时我们需要将不同类型的数据放在一个数组中,就可以使用 ts 新增类型 —— 元组 —— 固定长度并且制定数据类型的数组

注意,一个元组的元素的个数、类型和顺序,都应该和定义这个元组时的个数、类型和顺序一致:

let tuple: [string, number] = ['Jay', 22]

举个使用元组的好处的例子,比如我想将上面的第二项 22 重新赋值为保留两位小数的数字 22.00,于是我直接这样写 tuple[1] = tuple[1].toFixed(2),结果报错了:

image.png

因为 toFixed 的返回值是使用定点表示法表示给定数字的字符串,而不是数字。因为使用了元组,ts 帮我发现了这个错误。

enum(枚举)

ts 新增类型,使用枚举类型可以为一组数值赋予友好的名字,比如后端定义了几种支付方式 —— paypal、alipay 和 wechat,值分别定义为 0、1 和 2,就可以用枚举定义,代码如下:

enum PayType {
  Paypal,
  Alipay,
  Wechat
}

function fn(payMethod: PayType) {
  console.log(payMethod)
}

fn(PayType.Paypal) // 0
console.log(PayType[1]) // Alipay,由枚举的值可以得到它的名字

注意:第 7 行的 PayType 代表的是类型,而第 11 行的 PayType.Paypal 代表的是值。

默认情况下,枚举是从 0 开始为元素编号,所以上例中 PayType.Paypal 的值默认就是 0,其余的成员会从 0 开始自动增 1。也可以手动修改这些值,比如:

enum PayType {
  Paypal = 1,
  Alipay,
  Wechat = 'wechat' // 也可以赋值为字符串
}

console.log(PayType.Alipay) // 2
console.log(PayType.Wechat) // wechat

object

object 表示非原始类型,也就是除 numberstringbooleansymbolnullundefined 之外的类型,如果给一个 object 类型的变量赋值了原始类型则会报错,如下图:

image.png

注意:

  • 其实直接给对象类型定义为 object 会有问题,比如想获取 obj.name 时,编译器会认为 object 类型是不存在 name 属性的而报错:

image.png

  • 如果直接给一个没有指定类型的空对象添加键值对,会报错:

image.png

image.png

解决办法是给对象一个 any 类型:

let modelObj: any = {}
for (const item of queryFormItems) {
  modelObj[item.key] = item.value
}

对象类型

所以我们应该这样定义对象的类型,name: stringage: number 之间使用 ; 分割也可以:

let obj: { name: string, age: number } = { name: 'Jay', age: 18 }

解构赋值时定义类型

这里顺便举个例子说明在对一个对象解构赋值时如何定义类型:

const { name, age }: { name: string; age: number } = obj 

如果直接像下面这样写,则是给 name 起别名为 string,给 age 起别名为 number

const { name: string, age: number } = obj 

type(类型别名)

还可以使用 type 关键字定义类型别名的写法来定义,有换行的话 ; 还可以省略:

type ObjType = {
  name: string
  age?: number
  readonly gender: string
}
let obj: ObjType = { name: 'Jay',gender: '男' }

第 3 行给 age 后面添加 ? 代表是可选的;第 4 行的 readonly 表示该属性是只读的。

函数

关于函数的内容比较多,我在《TS 中的函数》这篇文章有单独介绍。

void

表示没有值或 undefined,一般用于定义函数的返回值不能是 undefinednull 之外的值,但因为有类型推断,这种情况 void 也可以省略不写:

function fn(): void {
  console.log(1)
  // 如果这里 return 1 则报错,return undefined 或 null 则是可以的
}

但如果将上面的函数改写成下面这样,则 return 1 也不会报错:

const fn: () => void = () => {
  return 1
}

这是因为像 fn 这样的箭头函数经常用作为回调函数,ts 会基于上下文类型推导出函数的返回类型,此时定义 fn 的返回值为 void,并不会强制 fn 一定不能有返回内容。

never

永远不会出现的值的类型。比如一个函数如果是死循环或者抛出了一个异常,那么这个函数就不会返回任何值,其返回值的类型就是 never

function fn(): never {
  throw '异常'
}

function foo(): never {
  while (true) { }
}

我在项目中比较常用到 never 类型的应用场景是在结合 switch...case 使用的时候,我喜欢在 default 里定义一个类型为 never 的值 x 并返回:

enum PayType {
  Alipay,
  Wechat
}

function processPay(payMethod: PayType) {
  switch (payMethod) {
    case PayType.Alipay:
      console.log('支付宝')
      break;
    case PayType.Wechat:
      console.log('微信')
      break;
    default:
      const x: never = payMethod
      return x
  }
}

这样做的好处是,当之后有一天,如果我给 PayType 添加了一个值 Paypal,但是忘记在 switch...case 里去对应地写个 case 来处理,那么 ts 就会提醒我:

联合类型

表示取值可以为多种类型中的一种,使用 | 来连接多个类型。 如下代码即表示变量 x 既可以是 number 类型也可以是 string 类型

let x: number | string = 22
x = 'Jay'

使用联合类型的时候,有时候需要通过 if 判断对类型进行缩小。

交叉类型

使用 & 来使多个类型结合在一起。一般是连接多个接口,如下例中类型为 personType 的对象 person 就需要同时有 singrun 方法:

interface ISinger {
  sing(): void
}
interface IRunner {
  run(): void
}

type personType = ISinger & IRunner
const person: personType = {
  sing() { },
  run() { }
}

字面量类型

一个字符串本身也可以作为类型,叫做字面量类型。字面量类型一般结合联合类型一起使用:

let direct: 'left' | 'right' = 'left'
direct = 'right'

如此,就不能给 direct 赋值为 'top' 之类的字符串,而只能是 'left''right'

字面量推理

现在假设有个字面量类型 directType

type directType = 'left' | 'right'

有个函数需要传入的参数的类型为 directType

function fn(direct: directType) {
  console.log(direct)
}

那么如果有一个对象 obj,其属性 left 的值刚好是 'left',那么照理应该是可以作为参数传给 fn 的,但其实是会报错:

image.png

因为 obj 的类型推断如下,所以 obj.left 的类型为 string,不符合 directType

image.png

解决的办法可以用类型断言 fn(obj.left as directType), 也可以用字面量推理:

const obj = {
  left: 'left'
} as const

加了 as const 后,相当于将 objleft 属性的类型变为了字面量类型:

image.png

感谢.gif 点赞.png