# 回顾
类型注解是 TypeScript 中一种用于给变量、函数参数、函数返回值等添加类型信息的语法。
通过类型注解,可以对数据进行类型约束。而 TypeScript 在编译时可以通过检查类型的正确性,来提供更好的代码提示和错误提示。
# 一、TS 数据类型
为了方便查阅,此处记录一下常用的 ts 知识点:
- 联合类型
- 交叉类型
- 可选、只读等
- ……
// 1、
let id: string | number = '33' // 联合类型(和unknown类型一样,后续可能会进行 类型缩小)
// 2、
type MyObject = { name: string } & { age: number } // 交叉类型
// 3、
type PersonType = {
username: string
age?: number // 可选属性
readonly height: number // 只读属性
}
# 1、常规类型
- 常规类型
let str: string = 'lencamo'
let age: number = 37
let sentence: string = `Hello, my name is ${age}`
let num: number = 20
let bool: boolean = true
let notSure: unknown = 4
let value: any = true
let nu: null = null
let ud: undefined = undefined
# 2、特殊类型
- any 类型
关于 any 类型
有一种说法就是,如果你不会 typescript 的话,就把它当做 anyscript 用吧 😂
在 ts 中,我们可以对 any 类型进行任何操作(包括获取不存在的属性、方法)
const arr: any[] = ['lencmo', 20, 1.8, {}]
let temp: any = []
temp = 'failure'
console.log(temp.length)
temp = 123
console.log(temp.split) // undefined 问题 🤔
对于 any 的使用,我个人是持开放态度的:
- 一方便如果随意使用 any 类型,会失去 ts 在类型检测的优势
- 但另一方面在一些特殊的场景下,强硬的使用 ts 类型注解反而会增加我们的工作量
比如:后端传过来的 dataList 数据、引入一些第三方库时类型注解缺失等场景
关于 unknown 类型
unknown 类型和 any 不同的是,unknown 可以解决上面 any 类型使用中遇到的问题。
let temp: unknown = []
temp = 'failure'
if (typeof temp === 'string') {
console.log(temp.length)
console.log(temp.split)
}
所以unknown
类型通常用于一些动态的场景,如处理来自外部的数据,需要先对数据类型进行判断之后再进行处理(过滤)。
我们可以通过进行 typeof 、比较或者更高级的类型检查来将其的类型范围缩小(类型缩小)
关于 never 类型
never 使用的场景非常少,通常在一些扩展工具的封装(详见后续的 ts 内置工具)中会使用。
// 需求:类型取反(不要传number类型的参数)
// - 完整版
function demo<T>(x: T extends number ? never : T) {
//
}
// - 简介版
type Exclude<T, E> = T extends E ? never : T
function demo<T>(x: Exclude<T, Number>) {
//
}
demo(1) // Argument of type 'number' is not assignable to parameter of type 'never'.
demo('1')
demo({ num: 1 })
# 3、元组/枚举
- 元组(python 中)
let arr: [number, string] = [1, 'c']
// 对比any
let arr: any[] = [1, 'c']
元组类型应用
在某些特定的场景中,我们可以在函数中返回多个值的时候,可以使用 元组类型
function useState<T>(initialState: T): [T, (newValue: T) => void] {
let stateValue = initialState
const setValue = (newValue: T) => {
stateValue = newValue
}
return [stateValue, setValue]
}
const [count, setCount] = useState(10)
console.log(count)
const [banners, setBanners] = useState<any[]>([])
console.log(banners)
- 枚举(java 中)
enum Direction {
Up,
Down,
Left,
Right
}
console.log(Direction.Up) // 0
function move(direction: Direction) {
// ...
console.log(direction) // 1
console.log(Direction[direction]) // “Down”
}
move(Direction.Down)
拓展
- 默认值
默认为索引值,当然我们也可以自定义值内容
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
- 高级
enum Operation {
Read = 1 << 0, // 0 ---》 0001
Write = 1 << 1, // 2 ---》 0010
foo = 1 << 2 // 4 ---》 0100
}
# 4、数组类型
提示
Promise<string>
:通过使用 Promise 泛型来指定 Promise 对象返回值的类型为 string
下面的 |
是起到什么作用呢?
联合类型
function printId(id: number | string) {
if (typeof id === 'string') {
console.log(id.toUpperCase())
} else {
console.log(id)
}
}
// Both OK
printId(101)
printId('202')
let arr: number[] = [1, 2, 3] // 普通
let arr: Array<string> = ['a', 'b', 'c'] // new Array 万物皆对象
let arr: (string | number)[] = [1, 'c']
// let arr: [number, string] = [1, 'c'] // 元组类型(一一对应)🚩
那如果要在数组中使用对象呢?
答案
// let arr: 自定义结构类型[] = [自定义结构数据]
let arr: { username: string; age: number }[] = [
{ username: 'lencamo', age: 19 },
{ username: 'lencamo', age: 19 }
]
# 5、对象类型 🎈
虽然通常不会像下面一样对对象添加注解,但还是提一下吧:
let obj: { username: string; age?: number } = {
username: 'lencamo'
age: 19 // 可选类型
}
对于对象类型的数据,typescript 通常采用接口(interface)或类型别名(type alias)来定义一个对象的类型,并将其作为类型注解使用。
// 示例
function locationPrint(x: number, y: number) {
console.log(x)
}
// 升级:类型别名
type PointType = {
x: number
y: number
}
function locationPrint(point: PointType) {
console.log(point.x)
}
上面在数组中使用对象的案例,就是一个很好的演示。
应用示例
- JavaScript
function parseLyric(lyric) {
const lyricsArr = []
const currentDate = new Date()
lyrics.push({ time: currentDate, text: lyric })
return lyricsArr
}
- typescript
// 对象类型
type LyricType = {
time: number
text: string
}
function parseLyric(lyric: string): LyricType[] {
const lyricsArr: LyricType[] = []
const currentDate: Date = new Date()
lyrics.push({ time: currentDate, text: lyric })
return lyricsArr
}
严格字面量赋值检测(解析)🤙
描述:
https://github.com/microsoft/TypeScript/pull/3823
现象:
interface Iperson1 {
name: string
age: number
}
const info: Iperson1 = {
name: 'lencamo',
age: 20,
height: 180 // 报错
}
// =============
const obj = {
name: 'lencamo',
age: 20,
height: 180 // 不报错
}
const info1: Iperson1 = obj
严格字面量赋值检测(应用)🤙
已知:
存在已有类型定义,切在多个场景使用
// type.ts
export interface IModalConfig {
header: {
newTitle: string
editTitle: string
}
formItems: any[]
}
const modalConfig: IModalConfig = {}
问题:
先有一个场景,大部分类型定义和 IModalConfig 一样,但多了一个 pageName 怎么办?
// 方式1
const config = {
pageName: 'department', // 新增的没有定义类型的数据
header: {
newTitle: '新建部门',
editTitle: '编辑部门'
},
formItems: [{}, {}, ...]
}
const modalConfig: IModalConfig = config
// 方式2
interface IModalConfigPro extends IModalConfig {
pageName: string
}
const modalConfig: IModalConfig = {
pageName: 'department', // 使用新的类型对象 IModalConfigPro
header: {
newTitle: '新建部门',
editTitle: '编辑部门'
},
formItems: [{}, {}, ...]
}
索引签名 ✍
interface Iperson1 {
name: string
age: number
// 索引签名
[key: string]: any
// [index: number]: any
}
const info: Iperson1 = {
name: 'lencamo',
age: 20,
height: 180 // 不报错😂
}
# 二、函数
在 TypeScript 中,对于没有返回值的函数,可以不指定返回值类型为 void ,但还是建议加上:
原因
function logMessage(message: string) {
console.log(message)
}
const result = logMessage('Hello, TypeScript!')
// 若没有返回值:输出 undefined,类型🤔为 any
console.log(result)
除此之外,回调函数(匿名函数)的相关参数,可以不声明参数类型,会根据上下文自动推断类型(而并非隐式的 any)
内容
最好是不要加类型注解,因为通常情况下原生方法会根据上下文自动推断类型
const names: string[] = ['zhang', 'li', 'wang', 'ren']
names.forEach(function (item, index, arr) {
//
})
# 1、返回值
和 java 中一样,ts 中的 void 表示的是函数没有返回值
返回值 void
通常情况下不需要手动追加 void,但在下面的情况下使用 void 还是有必要的:
- 箭头函数
type FooType = () => void
const foo: FooType = () => {
//
}
// 对比
type FooType = () => undefined
const foo: FooType = () => {
return undefined
}
- 回调函数
type ExecFnType = (...args: any[]) => void
function delayExecFn(fn: ExecFnType) {
setTimeout(() => {
fn('触发了回调')
})
}
delayExecFn((msg) => {
console.log(msg)
})
function greet(data: Date): void {
console.log(`today is ${data}`)
}
greet(Date()) // 报错
greet(new Date())
function getNumber(): number {
return 26
}
函数 与 never 类型
- 永远不会返回值
function error(message: string): never {
while (true) {}
// throw new Error(message) // 返回值无法到达终点
}
- 从未被观察到的值
// 如果拓展了Boolean类型,但没有在case处理,ts会给出报错提示
// function handleMessage(message: string | number | boolean) {
function handleMessage(message: string | number) {
switch (typeof message) {
case 'string':
console.log(message.length)
break
case 'number':
console.log(message)
break
default:
const check: never = message // 此处的message为never类型
}
}
// 拓展
handleMessage(true)
# 2、可选参数
在 typescript 中,我们可以通过可选链操作符?
实现可选参数功能。那 JavaScript 是怎么实现可选参数的呢? —— 默认参数
答案
function greet(a, a = 6) {
console.log(a + b)
}
add(2)
add(2, 3)
在回调函数中,官方描述 (opens new window)是不建议使用 可选链操作符? ,因为在使用回调函数/匿名函数时,typescript 是不会对传入的函数类型的参数个数进行检测的
// 1、使用默认值 👍(和原生js一样)
function add(a: number, b: number = 6): void {
console.log(a + b)
}
add(2)
add(2, 3)
// 2、使用可选链操作符
function add(a: number, b?: number): void {
// if (!b) {
// b = 6
// }
console.log(a + b)
}
add(2) // 2 + undefined --> NaN
add(2, 3)
拓展:函数重载
函数重载可以限制函数参数出现的形式
// 编写重载签名
function add(arg1: number, arg2: number): number // 参数全部为 number类型
function add(arg1: string, arg2: string): string // 参数全部为 string类型
function add(arg1: any, arg2: any): any {
return arg1 + arg2
}
add(2, 3)
add('hello', ' boy!')
# 3、剩余参数
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + ' ' + restOfName.join(' ')
}
let employeeName = buildName('Joseph', 'Samuel', 'Lucas', 'MacKinzie')
# 4、函数注解
函数本身也是一个标识符,也应该有自己的类型
function foo(arg: number): number {
return 520
}
const foo: any = (arg: number): number => {
return 520
}
当然使用 any 是默认的情况下,我们对函数进行注解可以采用函数类型表达式 或者 函数调用签名:
// 1、函数类型表达式
// 格式:(参数列表) => 返回值
type FooType = (num1: number) => number
const foo: FooType = (arg: number): number => {
return arg
}
foo(123)
// 2、调用签名(对象的🤔角度看待函数)
interface IFoo {
name: string
// 格式:(参数列表): 返回值
(num1: number): number
}
const foo: IFoo = (arg: number): number => {
return arg
}
foo.name = 'xxx'
bar(123)
使用示例
- JavaScript
// 定义
function calc(calcFn) {
const num1 = 10
const num2 = 20
const result = calcFn(num1, num2)
console.log(result)
}
// 使用
function add(num1, num2) {
return num1 + num2
}
calc(add)
- typescript
// 定义
type CalcFnType = (num1: number, num2: number) => number
function calc(calcFn: CalcFnType) {
const num1 = 10
const num2 = 20
const result = calcFn(num1, num2)
console.log(result)
}
// 使用
// 方式1
function add(num1: number, num2: number) {
return num1 + num2
}
calc(add)
// 方式2:匿名函数(类型自动推导)
calc(function (num1, num2) {
return num1 + num2
})
# 5、参数校验 🤔
在使用回调函数/匿名函数时,typescript 是不会对传入的函数类型的参数个数进行检测的:
原因也好理解,就是有些回调函数的参数我们不是一定要使用,如果 ts 对所有参数进行检测的话,就意味着我们必须传入所有参数,在显然是不合理的。
const names: string[] = ['zhang', 'li', 'wang', 'ren']
names.forEach(function (item) {
//
})
// 案例
type CalcFnType = (num1: number, num2: number) => number
function calc(calcFn: CalcFnType) {
const num1 = 10
const num2 = 20
const result = calcFn(num1, num2)
console.log(result)
}
calc(function (num1) {
return num1 * 3
})
同时,关于 ts 对类型的检查是否报错(提示),取决于它的内部规则:
interface IPerson {
name: string
age: number
}
// const info: IPerson = {
// name: 'zhangsan',
// age: 20,
// address: 'xxx' // 报错
// }
// ============
const p = {
name: 'zhangsan',
age: 20,
address: 'xxx'
}
const info: IPerson = p // 不报错 ?
# 二、对象
前面我们已经提到了,若直接给对象加上类型注解,一是使用不灵活,二是不整洁,加上在实际项目中我们对对象的使用需求还是非常大。
- 不整洁
let obj: { username: string; age?: number } = {
username: 'lencamo'
// age: 19
}
- 不灵活
let data: {
code: number
message: string
data: {
list: {
id: number
name: string
}[]
}
} = {
code: 200,
message: 'success',
data: {
list: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]
}
}
所以,在 TypeScript 中,通常采用接口(interface)或类型别名(type alias)来定义一个对象的类型。
思考
其思路就是,将对对象的类型约束内容单独提取出来,单独进行维护。
# 1、interface 接口
下面演示一下 interface 接口是如何一步一步的将对象数据进行分割,单独管理的:
热身:ts 与 axios 二次封装
这里就可以先给大家避一下一个误区:
在 ts 文件中,并不强制要求对所有数据都要进行类型注解(但 typescript 官方建议尽量全部使用类型注解)。
import request from '@/utils/request'
interface IParams {
pageNum: number
pageSize: number
}
export function demoApi(data: IParams) {
return request({
method: 'post',
url: '/api/course/todos',
data
})
}
interface IParams {
list: {
id: number
name: string
}[]
}
interface IData {
code: number
message: string
data?: IParams // 使用接口
}
// 类型注解
let data: IData = {
code: 200,
message: 'success',
data: {
list: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]
}
}
进一步的,我们还可以使用类型扩展,扩大原本 InterDate 自定义类型的范围:
以下面为例:IRes 处理拥有自身的,还拥有了 IData 的类型注解
interface IRes extends IData {
children?: []
}
// 类型注解
let data: IRes = {
//
}
其实 interface 的 extends 功能,也可以通过交叉类型实现相同的效果
代码示例
// 接口(通过 继承extends)
interface Animal {
name: string
category: string
}
interface Bear extends Animal {
honey: boolean
}
const bear: Bear = {
name: 'wang',
honey: true
}
// 接口(通过 交叉类型&)
interface Animal {
name: string
category: string
}
interface Bear {
honey: honey: boolean
}
const bear: Animal & Bear = {
name: 'wang',
honey: true
}
# 2、类型别名 type
简单的,我们用 type alias 实现一下:
type List = {
list: {
id: number
name: string
}[]
}
type Data = {
code: number
message: string
data?: List // 使用接口
}
// 类型注解
let data: Data = {
code: 200,
message: 'success',
data: {
list: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]
}
}
在 type 中也可以使用交叉类型:
代码示例
// 类型别名(通过 交叉类型&)
type Animal = {
name: string
category: string
}
type Bear = Animal & {
honey: Boolean
}
const bear: Bear = {
name: 'wang',
honey: true
}
# 3、两者对比 🎈
总结 ✍
- interface 作为接口,可以让类型无限延伸,其扩展性更强
- type alis 类型别名,其本身就是一种类型,可以让类型使用变得更加灵活
# ① 接口优势
- 接口可以被类实现(面向对象),而类型别名不可以
代码示例
// 定义一个接口
interface IPerson {
name: string
age: number
sayHello: () => void
}
// 实现接口
class Person implements IPerson {
constructor(public name: string, public age: number) {}
sayHello() {
console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`)
}
}
// ===============
// 定义一个类型别名
type PersonType = {
name: string
age: number
sayHello: () => void
}
// 使用类型别名
let person: PersonType = {
name: 'Tom',
age: 18,
sayHello() {
console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`)
}
}
- 接口可以扩展(继承)已有类型,而类型别名不能
代码示例
type Animal = {
name: string
category: string
}
interface Bear extends Animal {
honey: boolean
}
const bear: Bear = {
name: '熊猫',
category: '熊科',
honey: true
}
- 接口可以定义多个同名的成员,最终会被合并为一个成员,而类型别名不能
简单的说就是:在声明对象的时候,interface 可以多次声明同一个接口名称
代码示例
interface Person {
name: string
}
interface Person {
age: number
}
const person: Person = {
name: 'Tom',
age: 20
}
提示
由于接口的可拓展性更强,所以如果是对象类型的声明,使用 interface 可能会更好。
# ② 类型别名优势
- 类型别名可以使用联合类型、交叉类型、元组等类型,而接口不可以
简单的说就是:type 类型的使用范围更广
代码示例
// 联合类型(可选择性)
type MyType = string | number
// 交叉类型(合并多个类型)
type MyObject = { name: string } & { age: number }
// 元组类型(一一对应)
type MyTuple = [string, number]
- 当需要使用类型断言时,类型别名比接口更适合,它可以指定多个类型之间的关系
类型别名本身就是一个类型,可以像其他类型一样进行类型断言,而接口并不是一个类型,不能直接进行类型断言。
代码示例
// 使用类型别名定义一个字符串数组类型
type StringArray = string[]
// 使用类型别名来定义一个函数类型
type MyFunc = (x: number, y: number) => number
// 将一个对象断言为 MyType 类型
const obj = { name: 'Alice', age: 30 } as MyType
提示
由于类型别名 type 使用范围非常广泛,所以如果是非对象类型的声明,我们可以使用 type。
# 四、其他
# 1、类型断言 as
类型断言:可以指定更加具体的类型
let someValue: any = 'this is a string'
// 写法1
let strLength: number = (<string>someValue).length
// 写法2
let strLength: number = (someValue as string).length
提示
类型断言与 JavaScript 的类型转换 貌似相似,但出发点却截然不同:
- 类型断言:是指定更加具体的类型(或者 不太具体的类型 any/unknown),是类型缩小/扩大的一种方式
- 类型转换:是通过触发内置函数直接锁定类型
下面为方便理解与类型转换,写了一个示例(该示例在开发中不推荐使用)
// 类型转换
const id = Number('20')
// 类型断言
// const id = '20' as number // (错误)
const id = '20' as unknown as number // (正确)
类型断言 as 在 DOM 元素中的应用
目标:
获取 DOM 元素
<img class="pic" />
- 元素选择器
const imgElm = document.querySelector('img')
// 类型缩小
if (imgElm != null) {
imgElm.src = 'xxxxxx'
}
// 非空断言
imgElm!.src = 'xxxxxx'
- 类选择器
// const imgElm = document.querySelector('img') // HTMLImageElement | null
// const imgElm = document.querySelector('.pic') // 此时imgElm类型的范围扩大到了 Element | null
// 类型断言
const imgElm = document.querySelector('.pic') as HTMLImageElement
imgElm.src = 'xxxxxx'
# 2、非空断言!
在 TypeScript 中,!表示“非空断言运算符”,它可以用来告诉编译器一个表达式一定不会为 null 或 undefined。
通常在使用可选参数中使用,并且使用的前提是:当前情况下你确定该属性一定有值
注意
使用非空断言时,一定要确保该属性一定有值(即:谨慎使用)
不为 null 就不讲了,在上面 DOM 元素中的应用中就提到了;下面写一个不为 undefined 的案例:
interface PersonType {
name: string
age: number
friend?: {
name: string
}
}
const info: PersonType = {
name: 'zhangsan',
age: 20
}
// 1、访问属性
console.log(info.friend?.name)
// 2、属性赋值
// 类型缩小(存在就执行)
if (info.friend) {
info.friend.name = 'xxxx'
}
// 非空断言(确定friend.name一定存在)
// info.friend!.name = 'xxxx'
# 3、字面量类型
文字类型的效果类似于 const 的作用,但它又有许多新的特性:
// JavaScript中
const x = 'content'
x = 'lencamo' // 报错
// typescript中
let x: 'content' = 'content'
x = 'lencamo' // 报错
上面的文字类型代码并不能很好的演示它带来的好处,下面看看其在文字联合使用的效果:
- 代码示例
记忆
下面的应用有点像 前面提到的枚举类型。
// 1、字面量类型
type AlignType = 'left' | 'right' | 'center'
function printText(s: string, alignment: AlignType) {
// ...
}
printText('hello World!', 'left')
printText('hello World!', 'top') // 报错
// 2、枚举类型
enum Align {
left = 'left',
right = 'right',
center = 'center'
}
function printText(s: string, alignment: Align) {
// ...
}
printText('hello World!', Align.left)
printText('hello World!', Align.top) // 报错
字符串类型 和 文字类型 冲突问题
// method要求 字面量类型
function handleRequest(url: string, method: 'GET' | 'POST'): void {
//
}
const req = {
url: 'https://example.com',
method: 'GET'
}
handleRequest(req.url, req.method) // 报错 (传入的是字符串类型)
// 方式1
const req = {
url: 'https://example.com',
method: 'GET' as 'GET' // 断言为具体的 字面量类型
}
// 方式2
const req = {
url: 'https://example.com',
method: 'GET'
} as const // 断言为具体的 字面量类型