类型声明和类型检测:
在 TypeScript 中,通过类型声明来指定变量的类型,指定类型后,当为变量赋值时,TS 编译器会自动进行类型检测,检查值是否符合指定的类型,符合则赋值,否则报错。其中,使用 :
来指定变量的类型,:
前后有没有空格都可以;指定的类型可以称之为类型注解(Type Annotation)。
语法为:var/let/const 变量名: 类型 = 变量值
。
TypeScript 对于很多类型的检测报不报错,取决于它的内部规则。TypeScript 版本也在不断地更新:在进行合理的类型检测的情况下,同时让 TypeScript 更好用,在它们之间寻求一个平衡点。
简而言之,类型声明给变量设置了类型,设置类型后会自定进行类型检测,使得变量只能存储某种类型的值。
let name: string = 'Lee' // 声明一个变量 name,同时指定它的类型为 string。
name = 'Mary' // 正确
name = 10 // 报错
TypeScript 中进行类型检测的时候,使用的是鸭子类型。
鸭子类型:如果一只鸟,走起来像鸭子,游起来像鸭子,看起来像鸭子,那么就可以认为它是一只鸭子。也就是说,只关心属性和行为,并不关心到底是不是是对应的类型。class Person { constructor(public name: string, public age: number) {} } class Dog { constructor(public name: string, public age: number) {} } function showPersonInfo (p: Person) { console.log(p.name, p.age) } showPersonInfo(new Dog('旺财', 3)) // 正确。要求传入的实例对象是 Person 类,实际传入的实例对象是 Dog 类,但是因为它们有相同的属性,因此不会报错
类型推断:
在声明一个变量时,如果有直接赋值,TypeScript 会根据值的类型推断出类型注解,这就是类型推断。
因此,在开发过程中:
- 如果声明一个变量时直接对其进行赋值,可以省略类型注解。
- 通常情况下不需要明确函数的返回值的类型注解,TypeScript 会进行类型推断。
let 进行类型推断,推断出来的是通用类型;const 进行类型推断,推断出来的是字面量类型。
let name = 'Lee' // 根据值的类型推断出类型注解为 string
const num = 10 // 根据值的类型推断出类型注解为 10
类型别名:
类型别名用来给一个类型起个新名字。语法为 type 自定义类型名 = 类型
。类型别名常用于联合类型。
type customType = 1 | 2 | 3 | 4 | 5
let n1: customType
let n2: customType
n1 = 1
n2 = 5
类型断言:
类型断言可以用来手动指定一个值的类型。语法为 值 as 类型
或者 <类型> 值
。
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用
值 as 类型
。
此时,可以使用类型断言明确指定 imgEl 的类型为 HTMLImageElement。
let imgEl = document.querySelector('.img') as HTMLImageElement
imgEl.src = '' // 正确
TypeScript 允许类型断言由一种具体的类型断言为另一种不太具体的类型,或者由一种不太具体的类型断言为另一种具体的类型;但是不允许由一种具体的类型直接断言为另一种具体的类型,可以防止不应该发生的强制转换。
let num: number = 10
let num1 = num as string // 错误。从 number 类型断言为 string 类型,由一种具体的类型直接断言为另一种具体的类型
let num2 = num as any // 正确。从 number 类型断言为 any 类型,由一种具体的类型断言为另一种不太具体的类型
let num3 = num2 as string // 正确。从 any 类型断言为 string 类型,由一种不太具体的类型断言为另一种具体的类型
类型缩小(Type Narrowing):
把一个比较模糊的类型缩小为一个更加具体的类型,就叫做类型缩小。
常见的类型缩小的方式有:typeof、instanceof、switch case
、平等缩小(==
、===
、!=
、!==
)、in 等。
let id: number | string
console.log(id.length) // 报错。因为 id 可能是数字类型,也可能是 string 类型,是 number 类型的话是没有 length 属性的
if (typeof id === 'string') {
console.log(id.length) // 正确。通过类型缩小将 id 的类型确定缩小为 string 类型
}
TypeScript 中的类型:
TypeScript 中有多种类型,还支持自定义拓展类型。
基本类型:
TypeScript 中的基本类型有:字符串类型 string、数字类型 number、布尔类型 boolean、undefined、null、数组类型、对象类型、元组类型 tuple、任意类型 any、未知类型 unknown、空值类型 void、没有值类型 never。
TypeScript 中的基本类型名是小写的。
字符串类型 string:
表示任意字符串。
let name: string = 'Tom' // 正确
数字类型 number:
表示任意数字。
let num: number = 6.5 // 正确
布尔类型 boolean:
表示布尔值 true 或 false。
let flag: boolean = false // 正确
undefined:
表示 undefined 类型。
let u: undefined = undefined
null:
表示 null 类型。
let n: null = null
数组类型 array:
表示任意 JS 数组。语法为 类型[]
,或者也可以使用数组泛型 Array<类型>
。
let arr: string[]
arr = ['Lee‘, ’Mary‘] // 正确
arr = ['Lee‘, 18] // 错误
let arr1: Array<number> = [1, 1, 2, 3, 5] // 正确
对象类型 object:
表示任意的 JS 对象。但 object 类型不太实用,在开发中一般不使用。
// 在 JS 中一切皆对象
let obj: object
obj = {} // 正确
obj = function() {} // 正确
// 作为 object 对象类型的时候,既不能获取属性,也不能设置属性
let obj: object = {
name: 'Lee'
}
console.log(obj.name) // 报错
可以使用 {}
来代替,语法为:
{
属性名: 类型,
属性名?: 类型, // 可选属性。 ? 表示这个属性是可选的
readonly 属性名: 类型, // 只读属性。只能在创建对象的时候被赋值,其他时候被赋值就会报错
[index: string]: any, // 任意属性。这种写法可以称之为是索引签名,表示任意多个属性名的类型为 stirng,属性值的类型为 any 的属性。其中,index 只是一个标识符,可以任意命名;属性名的类型只能是 string 或者 number 其中一个。
}
// 直接赋值是,赋的值必须与 {} 中指定的属性名和类型一一对应,否则就会报错
let p: {name: string, age: number}
p = {name: 'Lee', age: 18}
// 可选属性:如果赋的值想要省略某个属性,可以在 {} 中指定的属性名后面加个 ?,表明这个属性是可选的
let p: {name: string, age?: number}
p = {name: 'Lee'}
// 只读属性
let p: {readonly name: string, age: number}
p = {name: 'Tom', age: 25}
p.name = 'Lee' // 报错
// 任意属性:如果赋的值中除了要有 name,还想要有任意多个任意类型的属性,可以使用 [index: string]: any 来表示
let p: {name: string, [index: string]: any}
p = {name: 'Lee', age: 18, sex: '男'}
// 一旦定义了任意属性,那么其他属性中符合任意类型属性名类型的,属性值类型也必须符合。
let p: {name: string, age: number, [index: string]: string}
p = {name: 'Tom', age: 25, gender: 'male'} // 报错。任意属性的属性名类型是 string,属性值类型是 string。但是age 的属性名类型是 string,属性值类型是 number
let p: {name: string, age: number, [index: number]: string}
p = {name: 'Tom', age: 25, gender: 'male'} // 正确。任意属性的属性名类型是 number,属性值类型是 string。其他属性的属性名都是 string,不需要匹配任意属性的类型
对于对象的字面量赋值,在 TypeScript 中有一个现象,叫做严格的字面量赋值检测。每个对象字面量在第一次创建时都会被认为是新鲜的,TypeScript 对其进行严格的类型检测,必须完全满足类型要求;当其不再新鲜后,TypeScript 就不会对其进行严格的类型检测了。
type PersonType = {
name: string,
age: number,
}
const p1: PersonType = {
name: 'Lee',
age: 18,
height: 1.88, // 报错。对于第一次创建的对象字面量,TypeScript 会将其标识为新鲜的,对其进行严格的类型检测,必须完全满足类型要求
}
// 此处是新鲜的,但是是在类型推断,并没有明确为其定义类型
const info = {
name: 'Lee',
age: 18,
height: 1.88, // 不报错
}
// 此处就不再是新鲜的了,不会对其进行严格的类型检测
const p2: PersonType = info // 不报错
Symbol 类型 symbol:
表示 Symbol 类型的值。
let s:symbol = Symbol()
元组类型 tuple:
元组是固定长度、固定类型的数组。语法为 [类型, 类型, ...]
。
let p: [string, number]
// 当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。
p = ['Lee', 25] // 正确
p = [25, 'Lee'] // 错误
p = ['Lee'] // 错误
p = ['Lee', 25, 10] // 错误
// 利用索引可以只赋值其中一项
p[0] = 'Lee' // 正确
// 元组是固定长度的
p[2] = 'Tom' // 错误
// 但是这种方法却不会报错,尽量不要使用
p.push('Mary') // 正确
p.push(10) // 正确
p.push(true) // 报错。当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型。
元组和数组的区别是:
- 数组中通常建议存放相同类型的元素;对于不同类型的元素更推荐存放在对象或者元组中。
- 元组中每个元素都有自己特定的类型,根据索引值获取到的值是可以确定其类型的;而数组中是无法确定其类型的。
任意类型 any:
表示任意类型的值。
如果无法确定一个变量的类型,或者变量的值的类型会发生改变,此时可以使用 any 类型。但是一个变量设置类型为 any 后相当于对该变量关闭 TypeScript 的类型检测,因此不建议使用 any 类型。
// 显式 any
let message: any
message = 'Lee' // 正确
message = 10 // 正确
// 隐式 any。声明变量但不赋值,如果此时不指定类型,则 TS 解析器会自动判断该变量的类型为 any。
let message
message = 'Lee' // 正确
message = 10 // 正确
未知类型 unknown:
表示类型安全的 any。
let message: unknown
message = 'Lee' // 正确
message = 10 // 正确
any 类型和 unknown 类型的区别是:在 any 类型的变量上直接做任何事都是合法的;在 unknown 类型的变量上直接做任何事都是非法的,必须要先类型缩小,经过校验,才能根据缩小之后的类型进行对应的操作。
let message: any = 'Lee'
let message1: string = message // 正确。message 的类型是 any,可以直接赋值给其他任意类型的变量
let message: unknown = 'Lee'
let message1: string = message // 报错。message 的类型是 unknown,不能直接赋值给其他任意类型的变量
let message: unknown = 'Lee'
if (typeof message === 'string') {
let message1: string = message // 正确。必须要先类型缩小,经过校验,才能根据缩小之后的类型进行对应的操作
}
let num: any = 10
console.log(num.length) // 正确
let num: unknown = 10
console.log(num.length) // 报错
let num: unknown = 10
if (typeof num === 'string') {
console.log(num.length) // 正确。必须要先类型缩小,经过校验,才能根据缩小之后的类型进行对应的操作
}
let num: unknown = 10
if (typeof num === 'number') {
console.log(num.length) // 报错。虽然经过了类型缩小,但没有进行对应类型的操作
}
空值类型 void:
表示空值,通常用来指定一个函数没有返回值。
function fn(): void {} // 正确
function fn1(): void {
return 123 // 报错
}
如果一个函数没有返回值,那么它的返回值的类型就是 void。
如果一个函数的返回值类型是 void,TypeScript 允许它返回 undefined。
function fn():void {
return undefined // 正确
}
没有值类型 never:
表示没有值。通常用来指定一个函数的返回值永远不会有任何结果,例如:抛出异常、死循环等。实际业务开发中基本不会用到,开发框架或者工具的时候才有可能会用到(可能会用于做一定的校验)。
function fn(): never {
throw new Error(‘出错了’) // 到这一行抛出异常,强行结束函数,函数的下一行不会执行到,函数永远执行不完
}
function fn1(): never {
while(true) {
console.log('死循环') // 死循环,函数永远执行不完
}
}
void 类型和 never 类型的区别是:void 类型是函数没有返回值;never 类型是函数永远执行不完。
字面量类型 literal:
限制变量的值就是该字面量的值。
let num: 10
num = 10 // 正确
num = 11 // 报错
内置对象:
JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。
内置对象是指根据标准在全局作用域上存在的对象。
- ECMAScript 的内置对象:ECMAScript 标准提供的内置对象有 Boolean、Error、Date、RegExp 等,可以在 TypeScript 中将变量定义为这些类型。
let b: Boolean = new Boolean(1) let e: Error = new Error('Error occurred') let d: Date = new Date() let r: RegExp = /[a-z]/
- DOM 和 BOM 的内置对象:DOM 和 BOM 提供的内置对象有 Document、HTMLElement、Event、NodeList 等。
let body: HTMLElement = document.body let allDiv: NodeList = document.querySelectorAll('div'); document.addEventListener('click', function(e: MouseEvent) {})
联合类型和交叉类型:
TypeScript 的类型系统允许使用多种运算符,从现有类型中构建新的类型。
联合类型:
联合类型表示满足多种类型中的一种即可。使用 |
来连接多个类型,表示或的关系。
let union: string | number
union = 'Lee' // 正确
union = 10 // 正确
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,只能访问此联合类型的所有类型里共有的属性或方法。
function getString(value: string | number) {
return value.toString() // 正确。toString() 是 string 和 number 的共有方法
}
function getLength(value: string | number) {
return value.length // 报错。length 不是 string 和 number 的共有属性
}
function getLength(value: string | number) {
if (typeof value === 'string') {
return value.length // 正确。可以使用类型缩小来解决
}
}
交叉类型:
交叉类型表示必须同时满足多种类型。使用 &
来连接多个类型,表示与的关系。
let cross: {name: string} & {age: number}
cross = {name: 'Lee‘, age: 18} // 正确
cross = {name: 'Lee'} // 报错
类型声明文件:
在 TypeScript 中,除了可以编写以 .ts
结尾的文件,还可以编写以 .d.ts
结尾的文件。
以 .d.ts
结尾的文件称之为类型声明文件,在这种文件中不写逻辑代码,只用来做类型声明,其中的类型声明全局都可直接使用。它的作用仅仅是用来告知 TypeScript 有哪些类型。
类型声明文件的分类:
类型声明文件可以分为以下几类:
- 内置的类型声明文件:是 TypeScript 自带的,内置了 JavaScript 运行时的一些标准化 API 的类型声明。例如:String、Date、Window、Document 等。
TypeScript 将其放置在
lib.[something].d.ts
文件中。
可以通过配置tsconfig.json
配置文件中的 target 和 lib 选项来决定哪些内置类型声明是可以使用的。 - 外部定义的类型声明文件:一般来自第三方库,这些库中可能有类型声明文件,也可能没有。
- 在库中已有类型声明文件:在 TypeScript 项目中引入这些库,库就可以直接使用。例如: axios。
- 在库中没有类型声明文件:在 TypeScript 项目中引入这些库,库不可以直接使用,会报错。此时,就需要额外再去安装这些库的类型声明的依赖包。例如:React。
- 在库中没有类型声明文件,并且也没有提供些库的类型声明的依赖包:在 TypeScript 项目中引入这些库,库不可以直接使用,会报错。此时,就需要开发者自己去写这些库的类型声明文件了。
文件名可以任意命名。
// 以 lodash 为例,lodash 有提供类型声明的依赖包,此处只是举例说明 // 新建 types.d.ts 文件 // 声明 lodash 模块 declare module "lodash" { export function join(...args: any[]): any }
- 开发者自己定义的类型声明文件。
类型声明文件中的语法:
类型声明文件中使用 declare 关键字来声明。
// 声明一个变量
declare const name: string
// 声明一个函数
declare function sum (num1: number, num2: number): number
// 声明一个类
declare class Person {
constructor(public name: string, public age: number)
}
// 声明一个模块,是为了可以在 TypeScript 文件中作为一个模块 import 导入使用
// 声明 lodash 为一个模块,就可以在 TypeScript 中 import 导入使用了,否则将会报错
declare module "lodash" {
// 在声明的模块中需要使用 export 关键字导出模块的属性和方法,就可以在外部通过模块名.属性/方法来调用
export function join(...args: any[]): any
}
// 声明以 png 结尾的文件为一个模块模块,就可以在 TypeScript 中 import 导入使用了,否则将会报错
declare module "*.png"
// 声明一个命名空间
// 例如:通过 cnd 的方式引入了 jQuery,此时是无法在 TypeScript 中直接使用 $ 的。可以通过声明一个命名空间 $,然后在其中导出 jQuery 需要使用到的属性和方法,就可以在全局使用 $.ajax() 了。
declare namespace $ {
export function ajax (settings: any): any
}