基础数据类型
JS 的八种内置类型
let str: string = 'jimmy'
let num: number = 31
let bool: boolean = true
let u: undefined = undefined
let n: null = null
let obj: object = { x: 1 }
let big: bigint = 100n
let sym: symbol = Symbol('me')
注意点
null 和 undefined
**默认情况下 null 和 undefined 是所有类型的子类型,就是说可以吧 null 和 undefined 复制给其他类型 **
let str: string = 'jimmy'
str = null
str = undefined
let num: number = 666
num = null
num = undefined
let obj: object = {}
obj = null
obj = undefined
let sym: symbol = Symbol('me')
sym = null
sym = undefined
let big: bigint = 100n
big = null
big = undefined
如果你在 tsconfig.json 指定了 “strictNullChecks”: true, null 和 undefined 只能赋值给 void 和 他们各自的类型
number 和 bigint
虽然 number 和 bigint 都表示数字,但是这两种类型不兼容
let big: bigint = 100n
let num: number = 6
big = num // 抛出一个类型不兼容的 ts(2322) 错误
num = big// 抛出一个类型不兼容的 ts(2322) 错误
其他类型
Array
对数组类型的定义有两种方式
let arr: string[] = ['1', '2']
let arr2: Array<string> = ['1', '2']
定义联合类型数组
let arr3: (number | string)[] = [1, 'b', 2, 'c']
定义指定对象成员的数组
interface Arrobj {
name: string,
age: number
}
let arr2: Arrobj[] = [{ name: 'jimmy', 'age': 31 }]
函数
函数声明
function sum(x: number, y: number): number {
return x + y
}
函数表达式
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y
}
用接口定义函数类型
interface SearchFunc{
(source: string, subString: string): boolean
}
可选参数
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName
} else {
return firstName
}
}
注意可选参数后面不允许再出现必需参数
参数默认值
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName
}
剩余参数
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item)
})
}
let a = []
push(a, 1, 2, 3) // [1, 2, 3]
函数重载
由于 Javascript 是一个动态语言,我们通常会使用不同类型的参数来调用同一个函数, 该函数会根据不同的参数而返回不同的类型的调用结果
function add(x, y) {
return x + y
}
add(1, 2) // 3
add('1', '2') // '12'
由于 Ts 是 Js 的超集,因此以上代码可以直接在 TS 中使用,但当 TS 编译器开启 noImpliciaAny 的配置时,以上代码会提示一下错误信息
Parameter 'x' implicitly has an 'any' type
Parameter 'y' implicitly has an 'any' type
该信息高速我们参数 x 和 参数 y 隐式具有 any 类型,为了解决这个问题,我们可以为参数设置一个类型, 因此我们希望 add 函数同事支持 string 和 number 类型,因此我们可以定义个 string | number 联合类型,同事我们可以为该类型取个别名
type Combinable = string | number
在顶一万 Combinable 联合类型后, 我们更新一下 add 函数
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString()
}
return a + b
}
为 add 函数的参数显式设置类型之后,之前的错误提示消息就消失了
const result = add('aaa', 'bbb')
result.split(' ')
在上面的代码中,我们分别使用 add 和 bbb 两个字符串作为参数调用 add 函数,并吧调用结果保存到一个名为 result 的变量中, 这时候我们想当然的认为此时 result 的变量类型为 string, 所以我们就可以正常调用字符串对象上的 split 方法, 但是此时 ts 编译器又出现一下错误信息
Property 'split' does not exist on type 'number'
很明显, number 类型上并不存在 split 属性,这时我们可以利用 ts 提供的函数重载特性
【函数重载或方法重载是使用相同的名称和不同类型参数数量或者类型创建多个方法的一种能力】。要解决前面遇到的问题,方法就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用
type Types = number | string
function add(a: number, b: number): number
function add(a: string, b: string): string
function add(a: string, b: number): string
function add(a: number, b: string): string
function add(a: Types, b: Types) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString()
}
return a + b
}
const result = add('aaa', 'bbb')
result.split(' ')
在以上代码中,我们为 add 函数提供了多个函数类型进行定义,从而实现了函数的重载,这个时候错误信息又消失了,因为此时 result 的变量的类型是 string 类型
Tuple(元组)
元组的定义
数组一般是有同种类型的值组成,但是有时我们需要再单个变量中存储不同变量的值, 这时候我们就可以使用元组, 在 JS 中是没有元组的, 元组是 TS 中特有的类型, 其工作方式类似于数组,
元组中最重要的特性是可以限制 数组元素的个数和类型,特别适合用来实现多指返回
元组用于保存定长定数据类型的数据
let x: [string, number]
// 类型必须匹配且个数必须为 2
x = ['hello', 10] // ok
x = ['hello', 10, 10] // Error
x = [10, 'hello'] // Error
注意,元组类型只能表示一个已知元素数量和类型的数组, 长度已指定,越界访问会提示错误,如果一个数组可能有多种类型,数量和类型都不确定, 那就直接 any[]
元组类型的结构赋值
我们可以通过下表的方式来访问元组中的元素, 当元组中的元素较多时,这种方式并不是那么便捷,其实元组也是支持结构赋值的
let employee: [number, string] = [1, 'Semlinker']
let [id, username] = employee
console.log(id, username)
这里需要注意的是,在解构赋值时,解构数组元素的个数是不能超过元组中的元素个数,否则会出现错误
元组类型的可选元素
与函数签名类似,在定义元组类型的时候, 我们可以通过 ? 号来声明元组类型的可选元素
let optionalTuple: [string, boolean?]
optionalTuple = ['jimmy', true] // ok
optionalTuple = ['jimmy'] // ok
在实际工作中,声明可选的元组元素有什么作用呢, 在三维坐标中,一个坐标点可以使用 (x, y, z) 的形式来表示,对于二维坐标来说, 坐标点可以使用 (x, y) 的形式来表示, 而对于一维坐标轴来说, 只需要使用 (x) 来表示即可, 针对这种情况, 在 ts 中就可以利用元组类型可选元素的特性来定义一个元组类型的坐标点,
type Point = [number, number?, number?]
const x: Point = [10]
const xy: Point = [10, 20]
const xyz: Ponit = [10, 20, 30]
元组类型的剩余元素
元组类型里最后一个元素可以是剩余元素,形式为 ...x, 这里 x 是数组类型。[剩余元素代表元组类型是开放的,可以有零个或多个额外的元素], 例如 [number, ...string[]], 表示带有一个 number 元素和任意数量 string 类型元素的元组类型,
typs RestTupleType = [number, ...string[]]
let restTuple: RestTupleType = [666, 'jimmy', 'lily','lucy']
只读的元素类型
Ts 还引入了对只读元组的新支持, 我们可以为任何元组类型加上 readonly 关键字前缀,使其成为只读元组
const point: readonly [number, number] = [10, 20]
在使用 readonly 关键字修饰元组类型之后,任何企图修改元组中的元组的操作都会抛出异常
point[0] = 1 // Cannot assign to '0' because it is a read-only property
ponit.push(0) // Property 'push' does not exits on type 'readonly [number, number]'
ponit.pop() // Property
void
void 表示没有任何类型,和其他类型是平等关系, 不能直接赋值
let a: void
let b: number = a // Error
只能为它赋予 null 和 undefined ,声明一个 void 类型的变量其实没有太大作用,我们一般也只有在函数没有返回值的时候去声明
值得注意的是,方法没有返回值将得到 undefined, 但是我们需要定义成 void 类型, 而不是 undefined 类型,否则将会报错
function fun(): undefined {
console.log('this is Ts')
}
fun() // Error
never
never 类型表示的是那些永远不存在的值得类型
值会永不存在的两种情况
- 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值,因为抛出异常会直接终端程序的允许,这使得程序运行不到返回值的那一步,即具有不可达的终点,也就永远不存在返回值
- 函数中执行无限循环的代码 死循环,使得程序永远无法允许到函数返回值的那一步,永不存在返回值
function err(msg: string): never {
throw new Error(msg)
}
function loopForever(): never {
while(true) {}
}
never 类型同 null 和 undefined 一样,也是任何类型的子类型, 也可以赋值给任何类型
但是没有任何类型是 never 的子类型或可以赋值给 never 类型(出了 never 本身之外), 即使 any 也不可以赋值给 never
let ne: never
let nev: never
let an: any
ne = 123 // Error
ne = nev // OK
ne = an // Error
ne = (() => { throw new Error('xxx') })() // OK
ne = (() => { while(true) {} })() // OK
在 TS 中,可以利用 never 类型的特性来实现全面性检查,
type Foo = string | number
function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === 'string') {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === 'number') {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check:never = foo
}
}
在 else 分支里面, 我们把收窄为 never 的 foo 值赋值给一个显示声明的 never 变量,如果一切逻辑正确,那么这里应该能够通过编译,但是假如后来有一天同事修改了 Foo 的类型
type Foo = string | number | boolean
然而忘记修改 controlFlowAnalysisWithNever 方法中的流程控制, 这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误,通过这个方式,我们可以确保 controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型,通过这时示例,我们可以得出一个结论 使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码
any
在 TS 中,任何类型都可以被归为 any 类型,这让 any 类型成为了类型系统的顶级类型,
如果是一个普通类型,在赋值过程中改变类型是不被允许的
let a: string = 'jimmy'
a = 7 // TS2322: Type 'number' is not assignable to type 'string'
但是如果是 any 类型,则允许被赋值给任意类型
let a: any = 666
a = 'Jimmy'
a = false
a = undefined
...
在 any 上访问任何属性都是被允许的,也允许调用任何方法
let anything: any = 'hello'
console.log(anything.myName)
anyThing.setName('Jerry')
在许多场景下,使用 any 类型,可以很容易的编写类型正确但是在允许时有问题的代码,如果我们使用 any 类型,就无法使用 ts 提供的大量的保护机制, 所以尽量不要使用 any 类型
为了解决 any 带来的问题, TS 引入了 unknown 类型
unknown
unknown 与 any 一样,所有类型都可以分配给 unknown
let noSure: unknown = 4
nosure = 'maybe a string instead' // OK
noSure = false // OK
unknown 与 any 最大的区别是,任何类型的值可以赋值给 any, 同时 any 类型的值也可以赋值给任何类型; unknown 任何类型的值都可以赋值给它, 但是它只能赋值给 unknown 和 any
let notSure: unknown = 4
let uncertain: any = notSure // OK
let notSure: any = 4
let uncertain: unknown = notSure // Error
let notSure: unknown = 4
let uncertain: number = notSure // Error
如果不缩小类型, 就无法对 unknown 类型执行任何操作
function getDog() {
return '123'
}
const dog: unknown = { hello: getDog }
dog.hello() // Error
这种机制起到了很强的预防性,更安全, 这就要求我们必须缩小类型, 我们可以使用 typeof , 类型断言等方式来缩小未知范围
function getDogName() {
let x: unknown;
return x
}
const dogName = getDogName()
// 直接使用
const upName = dogName.toLowerCase() // Error
if (typeof dogName === 'string') {
const upName = dogName.toLowerCase() // OK
}
// 类型断言
const upName = (dogName as string).toLowerCase() // OK
Number、 String、Boolean、Symbol
初学 TS 时, 很容易和原始类型 number、string、boolean、symbol 混淆的首字母大写的 Number、String、Boolean、Symbol 类型,后者是相应原始类型的包装对象,姑且把它们称之为对象类型
从类型兼容性上看,原始类型兼容对象的对象类型
所有原始类型的字面量本身也可作为类型使用,其外延只包括自身
let x: 0 = 0
let code: 'jimmy' = 'jimmy'
类型拓宽
所有通过 let 或 var 定义的变量,函数的形参,对象的非只读属性,如果满足指定了初始值且未显示添加类型注解的条件,那么他们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量拓宽
let str = 'this is string' // 类型是 string
let strFun = (str = 'this is string') => str // 类型是 (str?: string) => string
const specifiedStr = 'this is string' // 类型是 'this is string'
let str2 = specifiedStr // 类型是 string
let str2Fun2 = (str = specifiedStr) => str // 类型是 (str?: string) => string
因为第 1~2 行满足了 let、形参且未显示声明类型注解的条件,所以变量、形参的类型拓宽为 string (形参类型确切的将是 string | undefined)
因为第 3 行的常量不可变更,类型没有拓宽,所以 specifiedStr 类型是 'this is string' 字面量类型
第 4~5 行,因为赋予的值 specifiedStr 的类型是字面量类型,且没有显示类型注解,所以变量、形参的类型也被拓宽了,其实这样的设计符合实际的编程诉求,我们设想一下,如果 str2 的类型被推断为 'this is string', 它将不可变更,因为赋予任何其他的字符串类型的值都会提示类型错误
基于字面量类型的拓宽条件, 我们可以通过一下代码添加显示类型注解控制类型拓宽行为
const specifiedStr: 'this is string' = 'this is string' // 类型是 'this is string'
let str2 = specifiedStr // 即使使用了 let 定义,类型依然是 'this is string'
实际上,除了字面量类型拓宽之外,TS 对某些特定类型值也有类似的拓宽设计
比如对 null 和 undefined 的类型进行拓宽, 通过 let、var 定义的变量如果满足未显示生面类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any
let x = null // 类型拓宽为 any
let y = undefined // 类型拓宽为 any
const z = null // 类型是 null
let anyFun = (param = null) => param // 形参的类型是 null
let z2 = z // 类型是 null
let x2 = x // 类型是 null
let y2 = y // 类型是 undefined
假设你正在编写一个向量库,首先定义了一个 Vector3 接口, 然后定义了 getComponent 函数用于获取指定坐标轴的值
interface Vector3 {
x: number;
y: number;
z: number;
}
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
return vector[axis]
}
但是当使用 getComponent 函数时, TS 会提示以下错误信息
let x = 'x'
let vec = { x: 10, y: 20, z: 30 }
getComponent(vec, x) // Error 类型 string 的参数不能赋值给类型 'x' | 'y' | 'z'
因为变量 x 的类型被推断为 string, 而 getComponent 函数期望它的第二个参数有一个更具体的类型,这在实际场合中被拓宽了,导致了一个错误
这个过程是复杂的,因为对于任何给定的值都有许多可能的类型
const arr = ['x', 1]
上述 arr 变量的类型应该是什么, 这里有一些可能性
- ('x' | 1)[]
- ['x', 1]
- [string, number]
- readonly [string, number]
- (string | number) []
- readonly (string | number) []
- [any, any]
- any[]
没有更多的上下文,TS 无法知道那种类型是正确的,它必须猜测你的意图。尽管 TS 很聪明,但它无法读懂你的心思,不能 100% 正确,正如我们刚才看到的那样的错误
在下面的例子中,变量 x 的类型被推断为字符串, 因为 ts 允许这样的代码
let x = 'semlinker'
x = 'kakuqo'
x = 'lolo'
在推断 x 的类型为字符串时,ts 试图在特殊性和灵活性之间取得平衡,一般规则是,变量的类型在声明之后不应该改变,因此 string 比 string | RegExp 或 string | string[] 或任意字符串更有意义
ts 提供了一些控制拓宽过程的方法, 其中一种方法是使用 const , 如果用 const 而不是 let 声明的一个变量,那么它的类型会更窄,事实上, 使用 const 可以帮助我们修复前面的错误
const x = 'x' // 类型为 'x'
let vec = { x: 10, y: 20, z: 30 }
getComponent(vec, x) // OK
因为 x 不能重复赋值, 所以 ts 可以推断更窄的类型,就不会在后续赋值中出现错误,因为字符串字面量可以赋值给 'x' | 'y' | 'z',所以代码会通过类型检查器的检查
然而,const 并不是万能的,对于对象和数组,仍然会存在问题
以下代码在 js 中是没有问题的
const obj = {
x: 1
}
obj.x = 6
obj.x = '6'
obj.y = 8
obj.name = 'jimmy'
然而在 TS 中,对于 obj 的类型来说, 它可以是 { readonly x: 1 } 类型,或者更通用的 { x: number } 类型,当然也可能是 { [key: string]: number } 或者 obj 类型,对于对象, ts 的拓宽算法会将其内部属性视为将其赋值给 let 关键字声明的变量, 进而来推断其属性的类型, 因此 obj 的类型为 { x: number } ,这使得可以将 obj.x 赋值给其他 number 类型的变量,而不是 string 类型的变量, 并且它还会阻止你添加其他属性
ts 试图在具体性和灵活性之间取得平衡,它需要推断一个足够具体的类型来捕获错误,但又不能推断出错误的类型,它通过属性的初始值来推断属性的类型, 当然有几种方法可以覆盖 ts 的默认行为, 一种是提供显示的类型注释
const obj: {x: 1 | 3 | 5} = {
x: 1
}
另一种方法是使用 const 断言, 不要将其与 let 和 const 混淆, 后者在值控件中引入符号,这是一个纯粹的类型级构造,
// type is { x: number, y: number }
const obj1 = {
x: 1,
y: 2
}
// type is { x: 1, y: number }
const obj2 = {
x: 1 as const,
y: 2
}
// type is { readonly x: 1; readonly y: 2 }
const obj3 = {
x: 1,
y: 2
} as const
当在一个值后面使用 const 断言时,TS 将为它推断出最窄类型,没有拓宽,对于真正的常量,这通常是我们所需要的,当然也可以对数组使用 const 断言
const arr1 = [1, 2, 3] // number[]
const arr2 = [1, 2, 3] as const // readonly [1, 2, 3]
类型缩小
帮助类型检查器缩小类型的另一种常见方法是在他们上放置一个明确的标签
interface UploadEvent {
type: 'upload';
filename: string;
contents: string;
}
interface DownloadEvent {
type: 'download';
filename: string
}
type AppEvent = UploadEvent | DownloadEvent
function handleEvent(e: AppEvent) {
switch(e.type) {
case 'download':
xxx // type is DownloadEvent
case 'upload':
xxx // type is UploadEvent
}
}
这种模式也被称为联合标签或可辨识联合
类型别名
类型别名用来给一个类型起一个新名字,类型别名常用于联合类型
type Message = string | string[]
let greet = (message: Message) => { // ... }
类型别名, 我们仅仅是给类型取了一个新的名字, 并不是创建了一个新的类型
交叉类型
交叉类型是将锁哥类型合并为一个类型,这让我们可以把现有的多种类型叠加到一起称为一种新的类型,它包含了所需的所有类型的特征,使用 & 定义交叉类型
type Useless = string & number
交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果, 也就是所谓的合并接口类型,
type IntersectionType = { id: number; name: string; } & { age: number }
const mixed: IntersectionType = {
id: 1,
name: 'name',
age: 18
}
上述示例中,我们通过交叉类型,是的 IntersctionType 同时拥有了 id、name、age 所有的属性,这里我们可以试着将合并接口类型理解为求并集
思考
如果合并的多个接口类型存在同名属性会有什么效果
如果同名属性的类型不兼容,比如上面示例中两个接口类型同名的 name 属性类型一个是 number, 另一个是 string, 合并后, name 属性的类型就是 number 和 string 两个原子类型的交叉类型,即 never,
type IntersectionTypeConfict = { id: number: name: string } & { age: number; name: number }
const mixedConflict: IntersectionTypeConfict = {
id: 1,
name: 2, // ts(2333) 错误,'number' 类型不能赋值给 'never' 类型
age: 2
}
此时我们赋予 mixedConflict 任意类型的 name 属性值都会提示类型错误, 如果我们不设置 name 属性, 又会提示缺少一个必选的 name 属性的错误,这种情况下, 就意味着上述代码中的交叉出来的 IntersectionTypeConfict 类型是一个无用的类型
如果同名属性的类型兼容, 比如一个是 number, 一个是 number 的子类型, 数字字面量类型, 合并后 name 属性的类型就是两者中的子类型
如果同名属性是非基本数据类型的话,又会是什么情况呢?
interface A {
x: { d: true }
}
interface B {
x: { e: string }
}
interface C {
x: { f: number }
}
type ABC = A & B & C
let abc: ABC = { // OK
x: {
d: true,
e: '',
f: 666
}
}
由以上代码可知,在混入多个类型时,若存在相同的成员, 且成员类型为非基本数据类型,那么是可以成功合并的