# 回顾

类型检测 (opens new window)

  类型注解是 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 // 断言为具体的 字面量类型
更新于 : 8/7/2024, 2:16:31 PM