之前通过《TS 的安装与编译》介绍了我们如何在自己的电脑编写 ts 文件并把它们编译为 js。今天我们继续介绍些关于 ts 的知识,既然 ts 最主要的作用就是来给 js 做类型检测的,那么本文就来介绍下 ts 中的那些类型吧~
基本数据类型
TypeScript 是 JavaScript 的 超集,所以 ts 中也有字符串、数字、布尔值、undefined 和 null 这些基本数据类型,没啥好说的,但要注意两点:
- 类型注解(Type Annotation)的类型都是小写:
比如 const name: string = 'hello'
,如果是大写的 String
: const name: String = 'hello'
,也不会报错,但表示的是 js 中的 String
类,只不过可以赋值为一个字符串而已。
undefined
和null
是所有类型的子类型:
比如下面这样的写法是不会报错的(tsconfig.json 里关闭严格模式,"strict": false
)
let str: string = 'a'
str = 'b'
str = undefined
str = null
但如果变量本身的类型就是 undefined
或 null
,那么其值只能是 undefined
或 null
,也就是说它们既是值,也是类型:
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
类型的变量上进行任意的操作都是不合法的:
unknown
类型的变量,不可以直接赋值给 unknown
或 any
类型之外的变量:
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)
强制类型转换
我们还能通过类型断言来强制进行一些赋值。比如有个类型为 string
的 name
:
const name = 'Jay'
想把它赋值给类型为 number
的 age
,肯定会报错:
就可以通过类型断言将 name
断言为 number
,但是不能直接 name as number
:
这是因为 ts 只允许类型断言转换为更具体或更宽泛的类型,以防止不可能的强制转换,如果一定要强制转换,需要将 name
先转为 any
或 unknown
:
const age: number = (name as unknown) as number
数组
ts 中定义数组有两种方式:
- 方式一:
let 变量名: 数据类型[] = ['元素 1', '元素 2']
,数组里元素都只能为定义的类型,如果有不同类型会报错。
let arr: number[] = [1, 2, 3]
- 方式二:使用数组泛型(相当于是类使用了泛型),
let 变量名: Array<数据类型> = ['元素 1', '元素 2']
,注意这里Array
的A
是大写:
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)
,结果报错了:
因为 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
表示非原始类型,也就是除 number
、string
、boolean
、symbol
、null
和 undefined
之外的类型,如果给一个 object
类型的变量赋值了原始类型则会报错,如下图:
注意:
- 其实直接给对象类型定义为
object
会有问题,比如想获取obj.name
时,编译器会认为object
类型是不存在name
属性的而报错:
- 如果直接给一个没有指定类型的空对象添加键值对,会报错:
解决办法是给对象一个 any
类型:
let modelObj: any = {}
for (const item of queryFormItems) {
modelObj[item.key] = item.value
}
对象类型
所以我们应该这样定义对象的类型,name: string
和 age: 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
,一般用于定义函数的返回值不能是 undefined
和 null
之外的值,但因为有类型推断,这种情况 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
就需要同时有 sing
和 run
方法:
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
的,但其实是会报错:
因为 obj
的类型推断如下,所以 obj.left
的类型为 string
,不符合 directType
:
解决的办法可以用类型断言 fn(obj.left as directType)
, 也可以用字面量推理:
const obj = {
left: 'left'
} as const
加了 as const
后,相当于将 obj
的 left
属性的类型变为了字面量类型: