TypeScript学习 --- TS中的类型(2)

264 阅读7分钟

非空类型断言

我们确定传入的参数是有值的,这个时候我们可以使用非空类型断言

非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测

function getMessage(message?: string) {
  // 这种情况下message可能为空或者undefined,所以需要对message值为空的情况进行处理
  if(message) {
    // 类型缩小,所以此时的message的类型就是string
    console.log(message.toUpperCase())
  }
}
function getMessage(message?: string) {
  // 非空断言
  console.log(message!.toUpperCase())
}

可选链

  1. 可选链事实上并不是TypeScript独有的特性,它是ES11(ES2020)中增加的特性
  2. 可选链使用可选链操作符 ?.
  3. 它的作用是当对象的属性不存在时,会短路,直接返回undefined,如果存在,那么才会继续执行
  4. 可选链只能用于取值,不能用于赋值 ---> 即user?.age = 23这种写法是不合法的
type Person = {
  name: string,
  girlFriend?: {
    name: string
  }
}

const per: Person = {
  name: 'Klaus'
}

// per.girlFriend?.name <=> per.girlFirend && per.girlFirend.name
console.log(per.girlFriend?.name) // => undefined

?? 和 !!

const message = 'msg'

// !!message <=> Boolean(message)
console.log(!!message) //  true
// ??操作符 --- ES11增加的新特性
// 空值合并操作符(??)是一个逻辑操作符,当操作符的左侧是 null 或者 undefined 时,返回其右侧操作数, 否则返回左侧操作数
let msg: string | null = null

// msg ?? 'default value' 类似于 msg ? msg : 'default value' 或 msg || 'default value'
// 只不过msg为false的条件被限制为了 msg 为 null 和 undefined
// false ?? 'Klaus' => false
console.log(msg ?? 'default value') // => default value

msg = 'Hello World'

console.log(msg ?? 'default value') // => Hello World

字面量类型

let num1 = 123 // num1的类型为 number、
const num2 = 123 // num2的类型为 123 这个123就是字面量类型 也就会num2只能被赋值为123,不可以被赋值为其它的值
let num: 123
num = 123 // success
// num = 234 // error

但是实际开发中,这么做似乎是没有任何意义的,在实际开发中,字面量类型实际上是和联合类型一起组合使用

type Alignment = 'left' | 'right' | 'center'
const align: Alignment = 'left'

字面量推导

type Method = 'POST' | 'PUT' | 'GET' | 'UPDATE'

let request = {
  url: 'https://www.example.com/updateUser',
  method: 'POST'
}

/*
  此时request这个对象的类型会被推导为
  {
    url: string,
    method: string
  }
*/

function fetchInfo(url: string, method: Method) {
  // ...
}

// 此时request.method会报错的,因为此时request.method的类型是string类型,不是Method类型
fetchInfo(request.url, request.method)
type Method = 'POST' | 'PUT' | 'GET' | 'UPDATE'

let request = {
  url: 'https://www.example.com/updateUser',
  method: 'POST'
}

function fetchInfo(url: string, method: Method) {
  // ...
}

// 解决方法1: 类型断言 最常用 "大类型" -> "小类型"
fetchInfo(request.url, request.method as Method)
type Method = 'POST' | 'PUT' | 'GET' | 'UPDATE'

// 解决方式2: 字面量推导
let request = {
  url: 'https://www.example.com/updateUser',
  method: 'POST'
} as const

/*
  request的类型会被推导为字面量的形式

  let request: {
    readonly url: "https://www.example.com/updateUser";
    readonly method: "POST";
  }
*/

function fetchInfo(url: string, method: Method) {
  // ...
}

fetchInfo(request.url, request.method )
type Method = 'POST' | 'PUT' | 'GET' | 'UPDATE'

// 解决方式3: 使用手动类型注解替换默认的类型注解
type requestType = {
  url: string,
  method: Method
}

let request: requestType = {
  url: 'https://www.example.com/updateUser',
  method: 'POST'
}

function fetchInfo(url: string, method: Method) {
  // ...
}

fetchInfo(request.url, request.method )

类型缩小

类型缩小的英文是 Type Narrowing

可以通过类似于 typeof padding === "number" 的判断语句,来改变TypeScript的执行路径

在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为 缩小

而我们编写的 typeof padding === "number 可以称之为 类型保护(type guards)

常见的类型缩小有以下几种方式:

  1. typeof
  2. 平等缩小(比如===!==, ==, !=, switch)
  3. instanceof 和 xxx.constructor
  4. in
  5. ...

typeof

function print(val: number | string) {
  if (typeof val  === 'number') {
    console.log(val.toString())
  } else {
    console.log(val.toUpperCase())
  }
}

平等缩小

type Direction = 'left' | 'right' | 'up' | 'down'
function checkDirection(direction: Direction) {
  if (direction === 'left') {
    console.log('left', direction)
  } else if (direction === 'right') {
    console.log('right', direction)
  } 
  // ... 后边的逻辑判断省略
}

instanceof 和 xxx.constructor

class Teacher {
  teach() {
    console.log('teaching')
  }
}

class Student {
  learn() {
    console.log('learning')
  }
}

// 一般使用instanceof 很少使用xxx.constructor
function getAction(per: Teacher | Student) {
  if (per instanceof Teacher) {
    per.teach()
  } else {
    per.learn()
  }
}


function checkAction(per: Teacher | Student) {
  if (per.constructor === Teacher) {
    per.teach()
  } else if (per.constructor === Student) {
    // xxx.constructor 在这里并不能将per.constructor在else中直接判断为Student
    // 所以这里必须使用else if 而不是else
    per.learn()
  }
}

in

type Fish = {
  swim: () => void
}

type Cat = {
  run: () => void
}

// 这是只是字面量, 不是对象的实例
const fish: Fish = {
  swim() {
    console.log('swim')
  }
}

const cat: Cat = {
  run() {
    console.log('run')
  }
}


function getAction(animal: Cat | Fish) {
  if ('swim' in animal) {
    // 方法或属性 in 对象 --- 对象中是否有对应的属性或方法  --- 这是JS的语法特性,不是ts的
    animal.swim()
  } else {
    animal.run()
  }
}

函数类型

在JavaScript开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进 行传递)

在JavaScript中,函数可以作为一个基本的特殊变量来进行使用,所以函数也是需要有自己的类型注解

// () => void --- 就是函数的类型注解
function bar(fn: () => void) { fn() }
// ps:(num1: number, num2: number) => number 中 num1 和 num2 看起来是没有作用的
// 但是是不可以省略的, 如果写成了 (number, number) => number
// 那么对应的类型就会被认定为 (number: any, number: any) => number 从而出现类型错误
type SumFnType = (num1: number, num2: number) => number 
const sum: SumFnType = (num1: number, num2: number) => num1 + num2
// callback在被调用的时候,不需要指定参数的类型 会自动进行类型推断
[1, 2, 3].forEach(item => console.log(item))
// ? 就是一个可选参数 --- 可选参数必须放置在普通参数的后边
function print(num1: number, num2?: number) {
  console.log(num1, num2)
}
// num1 是有默认值的参数
// 默认参数推荐放置在最后,但是这并不是强制的
function print(num1: number = 10, num2: number) {
  console.log(num1, num2)
}

// 如果默认参数不是在最后的话,需要在默认参数对应的位置设置undefined作为占位符
print(undefined, 20)
/* 
  ...nums: number[] 是剩余参数,所有传入的剩余参数会被存放到nums这个数组中
  剩余参数必须是最后一个参数
*/
function sum(initalNum: number, ...nums: number[]) {
  // ...
}

this

可以正常推导出this的类型

let user = {
  name: 'Klaus',

  say() {
    // 这个函数是被定义在对象内部的
    // 所以这个this会被默认推导为是user这个对象
    console.log(this.name)
  }
}

user.say()

无法正常推导出this的类型

默认情况下,在tsconfig.json中有一个配置项 noImplicitThis

noImplicitThis这个配置项默认是开启的,

noImplicitThis这个配置项就是开启ts对于this的类型检测

如果noImplicitThis关闭了,ts就不会去对this进行类型检测

function say() {
  // 这个时候,ts是无法正常推导出this的类型
  // 因为say可以有多种调用方式
  // 例如: say() user.say() say.call() say.apply() ...
  // 所以在这种情况下,ts就无法正常推导出this的类型
  
  // console.log(this.name) ---> error
}

let user = {
  name: 'Klaus',
  say
}
type sayThisType = { name: string }

// 这个函数中通过this去调用了name属性
// 所以需要手动指定传入的this是一个含有name属性的对象
function say(this: sayThisType) {
  console.log(this.name)
}

let user = {
  name: 'Klaus',
  say
}

user.say()
say.call({ name: 'Alex' })
// say() ---> error
type sayThisType = { name: string }

// 如果需要传递多个参数的时候,this的类型指定必须为第一个参数
function say(this: sayThisType, age: number) {
  console.log(this.name, age)
}

let user = {
  name: 'Klaus',
  age: 23,
  say
}

user.say(user.age)
say.call({ name: 'Alex' }, 23)

函数重载

函数重载(overload)就是由一个函数名,但是参数不同的多个函数组成

在调用的时候,根据不同的参数去调用对应的函数实现逻辑

在函数重载中,参数不同的情况一帮可以被分为个数不同类型不同

// TS中函数重载 比较特殊 函数的类型定义和实现定义是分开的

// 函数类型定义 --- 又被称之为 重载签名(overload signatures)
function add(num1: number, num2: number): number
function add(num1: string, num2: string): string

// 函数实现定义
// 即便num1和num2的类型在定义的时候被设置为了any
// 但是因为之前有函数类型定义,也就是实现了重载
// 所以在调用的时候会从上往下依次进行匹配
// 如果没有匹配的类型定义,会直接报错
function add(num1: number | string, num2: number | string) {
  // 因为有了类型定义,所以只要对num1的类型判断好后,就基本可以确定num2的类型
  if (typeof num1 === 'string' && typeof num2 === 'string') {
    return num1 + ' ' + num2
  } else if (typeof num1 !== 'string' && typeof num2 !== 'string') {
    return num1 + num2
  }
}

console.log(add('Klaus', 'Wang')) // => Klaus Wang
console.log(add(2, 4)) // => 6

// console.log(add(2, 'Klaus')) ---> error  没有任何一个类型定义和它是匹配的