
一、原始类型:
avaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。
原始数据类型包括:布尔值、数值、字符串、null
、undefined
以及 ES6 中的新类型 Symbol
和 ES10 中的新类型 BigInt
。
基础类型
原始数据类型在 TypeScript 中的应用。
使用构造函数 Boolean
创造的对象不是布尔值:
1 | let createdByNewBoolean: boolean = new Boolean(1); |
事实上 new Boolean()
返回的是一个 Boolean
对象:
1 | let createdByNewBoolean: Boolean = new Boolean(1); |
直接调用 Boolean
也可以返回一个 boolean
类型:
1 | let createdByBoolean: boolean = Boolean(1); |
空值
JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void
表示没有任何返回值的函数:
1 | function alertName(): void { |
声明一个 void
类型的变量没有什么用,因为你只能将它赋值为 undefined
和 null
1 | let unusable: void = undefined; |
Null 和 Undefined
在 TypeScript 中,可以使用 null
和 undefined
来定义这两个原始数据类型:
1 | let u: undefined = undefined; |
与 void
的区别是,undefined
和 null
是所有类型的子类型。也就是说 undefined
类型的变量,可以赋值给 number
类型的变量:
1 | // 这样不会报错 |
1 | // 这样也不会报错 |
而 void
类型的变量不能赋值给 number
类型的变量:
1 | let u: void; |
联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。
1 | let myFavoriteNumber: string | number; |
访问联合类型的属性或方法
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:
1 | function getLength(something: string | number): number { |
上例中,length
不是 string
和 number
的共有属性,所以会报错。
访问 string
和 number
的共有属性是没问题的:
1 | function getString(something: string | number): string { |
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:
1 | let myFavoriteNumber: string | number; |
上例中,第二行的 myFavoriteNumber
被推断成了 string
,访问它的 length
属性不会报错。
而第四行的 myFavoriteNumber
被推断成了 number
,访问它的 length
属性时就报错了。
二、对象的类型——接口
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
简单的列子:
1 | interface Person { |
定义的变量比接口少了一些属性是不允许的:
1 | interface Person { |
多一些属性也是不允许的:
1 | interface Person { |
可见,赋值的时候,变量的形状必须和接口的形状保持一致。
可选属性
有时我们希望不要完全匹配一个形状,那么可以用可选属性:
1 | // 可选属性的含义是该属性可以不存在。 |
任意属性
1 | interface Person { |
使用 [propName: string]
定义了任意属性取 string
类型的值。
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集:
1 | interface Person { |
上例中,任意属性的值允许是 string
,但是可选属性 age
的值却是 number
,number
不是 string
的子属性,所以报错了。
一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:
1 | interface Person { |
只读属性
1 | interface Person { |
只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:
1 | interface Person { |
上例中,报错信息有两处,第一处是在对 tom
进行赋值的时候,没有给 id
赋值。
第二处是在给 tom.id
赋值的时候,由于它是只读属性,所以报错了。
三、数组的类型
表示法
「类型 + 方括号」
数组的项中不允许出现其他的类型:
1 | let fibonacci: number[] = [1, '1', 2, 3, 5]; |
数组泛型
1 | let fibonacci: Array<number> = [1, 1, 2, 3, 5]; |
用接口表示数组
1 | interface NumberArray { |
虽然接口也可以用来描述数组,但是我们一般不会这么做,因为这种方式比前两种方式复杂多了。
类数组
1 | function sum() { |
arguments
实际上是一个类数组,不能用普通的数组的方式来描述,而应该用接口:
1 | function sum() { |
事实上常用的类数组都有自己的接口定义,如 IArguments
, NodeList
, HTMLCollection
等:
1 | function sum() { |
四、函数类型
一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:
1 | function sum(x: number, y: number): number { |
输入多余的(或者少于要求的)参数,是不被允许的:
1 | function sum(x: number, y: number): number { |
函数表达式
如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样:
1 | let mySum = function (x: number, y: number): number { |
这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum
,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum
添加类型,则应该是这样:
1 | let mySum: (x: number, y: number) => number = function (x: number, y: number): number { |
注意不要混淆了 TypeScript 中的 =>
和 ES6 中的 =>
。
在 TypeScript 的类型定义中,=>
用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
用接口定义函数的形状
1 | interface SearchFunc { |
采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。
参数
可选参数:
可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了:
1 | function buildName(firstName?: string, lastName: string) { |
参数默认值:
TypeScript 会将添加了默认值的参数识别为可选参数:
1 | function buildName(firstName: string, lastName: string = 'Cat') { |
此时就不受「可选参数必须接在必需参数后面」的限制了:
1 | function buildName(firstName: string = 'Tom', lastName: string) { |
剩余参数:
1 | function push(array: any[], ...items: any[]) { |
注意,rest 参数只能是最后一个参数。
重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
1 | function reverse(x: number): number; |
上例中,我们重复定义了多次函数 reverse
,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。
五、类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型。
语法
1 | 值 as 类型 |
或
1 | <类型>值 |
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者,即 值 as 类型
。
建议大家在使用类型断言时,统一使用 值 as 类型
这样的语法
用途
1.将一个联合类型断言为其中一个类型
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法:
1 | interface Cat { |
而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,比如:
1 | interface Cat { |
此时可以使用类型断言,将 animal
断言成 Fish
:
1 | interface Cat { |
需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:
1 | interface Cat { |
上面的例子编译时不会报错,但在运行时会报错:
1 | Uncaught TypeError: animal.swim is not a function` |
原因是 (animal as Fish).swim()
这段代码隐藏了 animal
可能为 Cat
的情况,将 animal
直接断言为 Fish
了,而 TypeScript 编译器信任了我们的断言,故在调用 swim()
时没有编译错误。
可是 swim
函数接受的参数是 Cat | Fish
,一旦传入的参数是 Cat
类型的变量,由于 Cat
上没有 swim
方法,就会导致运行时错误了。
2.将一个父类断言为更加具体的子类
当类之间有继承关系时,类型断言也是很常见的:
1 | class ApiError extends Error { |
大家可能会注意到,在这个例子中有一个更合适的方式来判断是不是 ApiError
,那就是使用 instanceof
:
1 | class ApiError extends Error { |
上面的例子中,确实使用 instanceof
更加合适,因为 ApiError
是一个 JavaScript 的类,能够通过 instanceof
来判断 error
是否是它的实例。
但是有的情况下 ApiError
和 HttpError
不是一个真正的类,而只是一个 TypeScript 的接口(interface
),接口是一个类型,不是一个真正的值,它在编译结果中会被删除,当然就无法使用 instanceof
来做运行时判断了:
1 | interface ApiError extends Error { |
此时就只能用类型断言,通过判断是否存在 code
属性,来判断传入的参数是不是 ApiError
了:
1 | interface ApiError extends Error { |
3.将任何一个类型断言为 any
1 | window.foo = 1; |
上面的例子中,我们需要将 window
上添加一个属性 foo
,但 TypeScript 编译时会报错,提示我们 window
上不存在 foo
属性。
此时我们可以使用 as any
临时将 window
断言为 any
类型:
1 | (window as any).foo = 1; |
在 any
类型的变量上,访问任何属性都是允许的。
需要注意的是,将一个变量断言为 any
可以说是解决 TypeScript 中类型问题的最后一个手段。
它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any
。
4.将 any
断言为一个具体的类型
遇到 any
类型的变量时,我们可以选择无视它,任由它滋生更多的 any
。
我们也可以选择改进它,通过类型断言及时的把 any
断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展。
举例来说,历史遗留的代码中有个 getCacheData
,它的返回值是 any
:
1 | function getCacheData(key: string): any { |
那么我们在使用它时,最好能够将调用了它之后的返回值断言成一个精确的类型,这样就方便了后续的操作:
1 | function getCacheData(key: string): any { |
上面的例子中,我们调用完 getCacheData
之后,立即将它断言为 Cat
类型。这样的话明确了 tom
的类型,后续对 tom
的访问时就有了代码补全,提高了代码的可维护性。
限制
那么类型断言有没有什么限制呢?是不是任何一个类型都可以被断言为任何另一个类型呢?
答案是否定的——并不是任何一个类型都可以被断言为任何另一个类型。
具体来说,若 A
兼容 B
,那么 A
能够被断言为 B
,B
也能被断言为 A
。
下面我们通过一个简化的例子,来理解类型断言的限制:
1 | interface Animal { |
在上面的例子中,Cat
包含了 Animal
中的所有属性,除此之外,它还有一个额外的方法 run
。
TypeScript 并不关心 Cat
和 Animal
之间定义时是什么关系,而只会看它们最终的结构有什么关系——所以它与 Cat extends Animal
是等价的:
1 | interface Animal { |
那么也不难理解为什么 Cat
类型的 tom
可以赋值给 Animal
类型的 animal
了——就像面向对象编程中我们可以将子类的实例赋值给类型为父类的变量。
我们把它换成 TypeScript 中更专业的说法,即:Animal
兼容 Cat
。
当 Animal
兼容 Cat
时,它们就可以互相进行类型断言了:
1 | interface Animal { |
这样的设计其实也很容易就能理解:
- 允许
animal as Cat
是因为「父类可以被断言为子类」,这个前面已经学习过了 - 允许
cat as Animal
是因为既然子类拥有父类的属性和方法,那么被断言为父类,获取父类的属性、调用父类的方法,就不会有任何问题,故「子类可以被断言为父类」
总之,若 A
兼容 B
,那么 A
能够被断言为 B
,B
也能被断言为 A
。
同理,若 B
兼容 A
,那么 A
能够被断言为 B
,B
也能被断言为 A
。
综上所述:
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
- 要使得
A
能够被断言为B
,只需要A
兼容B
或B
兼容A
即可
其实前四种情况都是最后一个的特例。
双重断言
既然:
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
那么我们是不是可以使用双重断言 as any as Foo
来将任何一个类型断言为任何另一个类型呢?
1 | interface Cat { |
在上面的例子中,若直接使用 cat as Fish
肯定会报错,因为 Cat
和 Fish
互相都不兼容。
但是若使用双重断言,则可以打破「要使得 A
能够被断言为 B
,只需要 A
兼容 B
或 B
兼容 A
即可」的限制,将任何一个类型断言为任何另一个类型。
若你使用了这种双重断言,那么十有八九是非常错误的,它很可能会导致运行时错误。
除非迫不得已,千万别用双重断言。
类型断言 vs 类型转换
类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除:
1 | function toBoolean(something: any): boolean { |
在上面的例子中,将 something
断言为 boolean
虽然可以通过编译,但是并没有什么用,代码在编译后会变成:
1 | function toBoolean(something) { |
所以类型断言不是类型转换,它不会真的影响到变量的类型。
若要进行类型转换,需要直接调用类型转换的方法:
1 | function toBoolean(something: any): boolean { |
keyof 类型操作符
对一个对象类型使用 keyof
操作符,会返回该==对象属性名组成的一个字符串或者数字字面量的联合==。
这个例子中的类型 P 就等同于 “x” | “y”:
1 | type Point = { x: number; y: number }; |
但如果这个类型有一个 string
或者 number
类型的索引签名,keyof
则会直接返回这些类型:
1 | type Arrayish = { [n: number]: unknown }; |
注意在这个例子中,M
是 string | number
,这是因为 JavaScript 对象的属性名会被强制转为一个字符串,所以 obj[0]
和 obj["0"]
是一样的。
typeof类型操作符
TypeScript 添加的 typeof
方法可以在类型上下文(type context)中使用,用于==获取一个变量或者属性的类型==。
1 | let s = "hello"; |
对对象使用 typeof
:
1 | const person = { name: "kevin", age: "18" } |
对函数使用 typeof
:
1 | function identity<Type>(arg: Type): Type { |
对 enum 使用 typeof
:
1 | enum UserResponse { |
不过对一个 enum 类型只使用 typeof
一般没什么用,通常还会搭配 keyof
操作符用于获取属性名的联合字符串:
1 | type result = keyof typeof UserResponse; |
- Post title: TypeScript基础
- Create time: 2021-11-13 16:35:00
- Post link: 2021/11/13/TypeScript基础/
- Copyright notice: All articles in this blog are licensed under BY-NC-SA unless stating additionally.