TS相关知识了解

概述

本篇我们来讲述一下TS中的常见但又有些难度的小知识。

联合类型 |

TypeScript 的类型系统允许我们使用多种运算符,从现有类型中构建新类型
我们来使用第一种组合类型的方法: 联合类型(Union Type)

  • 联合类型是由两个或者多个其他类型组成的类型
  • 表示 可以是这些类型中的 任何 一个值
  • 联合类型中的每一个被称之为联合成员(union’s members )
// 1.联合类型的基本使用
// let foo: number | string = "abc"
// foo = 123

// // 使用的时候要特别的小心
// if (typeof foo === "string") {
//   console.log(foo.length)
// }


// 2.举个栗子: 打印id
function printID(id: number | string) {
  console.log("您的ID:", id)

  // 类型缩小
  if (typeof id === "string") {
    console.log(id.length)
  } else {
    console.log(id)
  }
}

printID("abc")
printID(123)

export {}

类型别名 type

在前面,我们通过类型注解中编写对象类型和联合类型,但是当我们想要多次在其他地方使用时就编写多次。这时我们可以给对象类型起一个别名:

// 类型别名: type
type MyNumber = number
const age: MyNumber = 18

// 给ID的类型起一个别名
type IDType = number | string

function printID(id: IDType) {
  console.log(id)
} 

// 打印坐标
type PointType = { x: number, y: number, z?: number }
function printCoordinate(point: PointType) {
  console.log(point.x, point.y, point.z)
} 

export {}

接口的声明 interface

const name = "why"

console.log("why")
console.log(name)

type PointType = {
  x: number
  y: number
  z?: number
}

// 接口: interface
// 声明的方式
interface PointType2 {
  x: number
  y: number
  z?: number
}

function printCoordinate(point: PointType2) {
  
}

export {}

type与interface的区别

我们会发现interface和type都可以用来定义对象类型,那么在开发中时到底选择哪一个呢?

  • 如果是定义非对象类型,通常推荐使用type。
  • 如果是定义对象类型,那么他们有区别的:
    1. interface 可以重复的对某个接口来定义属性和方法;
    2. 而type 定义的是别名,不能重复。
// 1.区别一: type类型使用范围更广, 接口类型只能用来声明对象
type MyNumber = number
type IDType = number | string


// 2.区别二: 在声明对象时, interface可以多次声明
// 2.1. type不允许两个相同名称的别名同时存在
// type PointType1 = {
//   x: number
//   y: number
// }

// type PointType1 = {
//   z?: number
// }


// 2.2. interface可以多次声明同一个接口名称
interface PointType2 {
  x: number
  y: number
}

interface PointType2 {
  z: number
}

const point: PointType2 = {
  x: 100,
  y: 200,
  z: 300
}


// 3.interface支持继承的
interface IPerson {
  name: string
  age: number
}

interface IKun extends IPerson {
  slogan: string
}

const ikun1: IKun = {
  slogan: "你干嘛, 哎呦",
  name: "kobe",
  age: 30
}

// 4.interface可以被类实现(TS面向对象时候再讲)
// class Person implements IPerson {

// }

// 总结: 如果是非对象类型的定义使用type
// 如果是对象类型的声明那么使用interface

export {}

所以, interface可以为现有的接口提供更多扩展。

交叉类型 &

  • 交叉类似表示需要满足多个型的条件
  • 交叉类型使用 &符号
// 回顾: 联合类型
type ID = number | string
const id1: ID = "abc"
const id2: ID = 123

// 交叉类型: 两种(多种)类型要同时满足
type NewType = number & string // 没有意义

interface IKun {
  name: string
  age: number
}

interface ICoder {
  name: string
  coding: () => void
}

type InfoType = IKun & ICoder

const info: InfoType = {
  name: "why",
  age: 18,
  coding: function() {
    console.log("coding")
  }
}

export {}

类型断言 as

有时候TypeScript 无法获取具体的类型信息,这个时候我们需要使用类型断言(Type Assertions)
类型断言的规则: 断言只能断言成更加具体的类型, 或者不太具体(any/unknown) 类型。此规则可防止不可能的强制转换

// 获取DOM元素 <img class="img"/>
// const imgEl = document.querySelector(".img")
// if (imgEl !== null) { // 类型缩小
//   imgEl.src = "xxx"
//   imgEl.alt = "yyy"
// }

// 使用类型断言
const imgEl = document.querySelector(".img") as HTMLImageElement
imgEl.src = "xxx"
imgEl.alt = "yyy"

// 类型断言的规则: 断言只能断言成更加具体的类型
// 或者不太具体(any/unknown) 类型
const age: number = 18
// 错误的做法
// const age2 = age as string

// TS类型检测来说是正确的, 但是这个代码本身不太正确
// const age3 = age as any
// const age4 = age3 as string
// console.log(age4.split(" "))

export {}

非空类型断言言 !

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

// 定义接口
interface IPerson {
  name: string
  age: number
  friend?: {
    name: string
  }
}

const info: IPerson = {
  name: "why",
  age: 18
}

// 访问属性: 可选链: ?.
console.log(info.friend?.name)

// 属性赋值:
// 解决方案一: 类型缩小
if (info.friend) {
  info.friend.name = "kobe"
}

// 解决方案二: 非空类型断言
// (有点危险, 只有确保friend一定有值的情况, 才能使用)
info.friend!.name = "james"

export {}

字面量类型

// 1.字面量类型的基本使用:没有什么意义
const name: "why" = "why"
let age: 18 = 18

// 2.将多个字面量类型联合起来 |
type Direction = "left" | "right" | "up" | "down"
const d1: Direction = "left"

// 栗子: 封装请求方法
type MethodType = "get" | "post"
function request(url: string, method: MethodType) {
}

request("http://xxx.xx.xx", "post")

// TS细节
// const info = {
//   url: "xxxx",
//   method: "post"
// }
// 下面的做法是错误: info.method获取的是string类型
// request(info.url, info.method)

// 解决方案一: info.method进行类型断言
// request(info.url, info.method as "post")

// 解决方案二: 直接让info对象类型是一个字面量类型
// const info2: { url: string, method: "post" } = {
//   url: "xxxx",
//   method: "post"
// }
const info2 = {
  url: "xxxx",
  method: "post"
} as const
// xxx 本身就是一个string
request(info2.url, info2.method)

export {}

类型缩小

什么是类型缩小呢?

  • 类型缩小的英文是 Type Narrowing(也有人翻译成类型收窄);
  • 我们可以通过类似于 typeof padding === “number” 的判断语句,来改变 TypeScriptTypeScript 的执行路径 ;
  • 在给定的执行路径中,我们可以缩小比声明时更小的类型 ,这个过程称之为缩小(Narrowing )
  • 而我们编写的 typeof padding === “number” 可以称之为 类型保护(type guards )。
    常见的类型保护有如下几种:
  • typeof
  • 平等缩小(=!
  • instanceof
  • in
// 1.typeof: 使用的最多
function printID(id: number | string) {
  if (typeof id === "string") {
    console.log(id.length, id.split(" "))
  } else {
    console.log(id)
  }
}

// 2.===/!==: 方向的类型判断
type Direction = "left" | "right" | "up" | "down"
function switchDirection(direction: Direction) {
  if (direction === "left") {
    console.log("左:", "角色向左移动")
  } else if (direction === "right") {
    console.log("右:", "角色向右移动")
  } else if (direction === "up") {
    console.log("上:", "角色向上移动")
  } else if (direction === "down") {
    console.log("下:", "角色向下移动")
  }
}

// 3. instanceof: 传入一个日期, 打印日期
function printDate(date: string | Date) {
  if (date instanceof Date) {
    console.log(date.getTime())
  } else {
    console.log(date)
  }

  // if (typeof date === "string") {
  //   console.log(date)
  // } else {
  //   console.log(date.getTime())
  // }
}

// 4.in: 判断是否有某一个属性
interface ISwim {
  swim: () => void
}

interface IRun {
  run: () => void
}

function move(animal: ISwim | IRun) {
  if ("swim" in animal) {
    animal.swim()
  } else if ("run" in animal) {
    animal.run()
  }
}

const fish: ISwim = {
  swim: function() {}
}

const dog: IRun = {
  run: function() {}
}

move(fish)
move(dog)

export {}

函数类型

函数类型的表示方法

function foo(arg: number): number {
  return 123
} 

export {}

如上例所示,函数foo的参数和返回值都有自己的类型,但是foo本身也是一个标识符, 也应该有自己的类型。

// 格式: (参数列表) => 返回值
type BarType = (num1: number) => number
const bar: BarType = (arg: number): number => {
  return 123
}

export {}

函数类型参数的格式
有两条tips:

  1. TypeScript对于传入的函数类型的多余的参数会被忽略掉
  2. TS对于很多类型的检测报不报错, 取决于它的内部规则,TS版本在不断更新: 在进行合理的类型检测的情况,让ts同时更好用(好用和类型检测之间找到一个平衡)
// TypeScript对于传入的函数类型的多余的参数会被忽略掉
// (the extra arguments are simply ignored.)
type CalcType = (num1: number, num2: number) => number
function calc(calcFn: CalcType) {
  calcFn(10, 20)
}

calc(function(num) {
  return 123
})

// forEach栗子:
const names = ["abc", "cba", "nba"]
names.forEach(function(item) {
  console.log(item.length)
})

// TS对于很多类型的检测报不报错, 取决于它的内部规则
// TS版本在不断更新: 在进行合理的类型检测的情况
// 让ts同时更好用(好用和类型检测之间找到一个平衡)
// 举一个栗子:
interface IPerson {
  name: string
  age: number
}

// typescript github issue, 成员
const p = {
  name: "why",
  age: 18,
  height: 1.88,
  address: "广州市"
}

// 这里之所以不报错,按照TS团队成员在GitHub上的回答就是:
// 因为此时p不是初次赋值,不fresh了
const info: IPerson = p

export {}

调用签名

在TypeScript 中,函数除了可以被调用,自己也是可以有属性值的。

  • 然而前面讲到的函数类型表达式,并不能支持声明属性
  • 如果我们想描述一个带有属性的函数,我们可以在一个对象类型中写一个调用签名( call signature)
// 1.函数类型表达式
type BarType = (num1: number) => number

// 2.函数的调用签名(从对象的角度来看待这个函数, 也可以有其他属性)
interface IBar {
  name: string
  age: number
  // 函数可以调用: 函数调用签名
  (num1: number): number
}

const bar: IBar = (num1: number): number => {
  return 123
}

bar.name = "aaa"
bar.age = 18
bar(123)

// 开发中如何选择:
// 1.如果只是描述函数类型本身(函数可以被调用)
//	 使用函数类型表达式(Function Type Expressions)
// 2.如果在描述函数作为对象可以被调用, 同时也有其他属性时
// 	 使用函数调用签名(Call Signatures)

export {}

注意这个语法跟函数类型表达式稍有不同,在参数列表和返回的类型之间用的是:而不是=>

构造签名

JavaScript函数也可以使用 new 操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数 ( constructors), 因为他们会产生一个新对象。
可以写一个 构造签名(Construct Signatures )方法,是在 调用签名 加一个 new 关键词;

class Person {
  name: string,
  constructor(name: string) {
	this.name = name
  }
}

interface IPerson {
  new (name: string): Person
}

function factory(P: IPerson) {
  return new P('Lee') 
}

函数的参数:可选参数

// y就是一个可选参数
// 可选参数类型是什么? number | undefined 联合类型
function foo(x: number, y?: number) {
  if (y !== undefined) {
    console.log(y + 10)
  }
}

foo(10)
foo(10, 20)

export {}

函数的参数:参数默认值

函数的参数可以有默认值

  1. 有默认值的情况下, 参数的类型注解可以省略
  2. 有默认值的参数, 是可以接收一个undefined的值
// 这个时候的y是number和undefined的联合类型
function foo(x: number, y = 100) {
  console.log(y + 10)
}

foo(10)
foo(10, undefined)
foo(10, 55)

export {}

函数的参数:剩余参数

从ES6 开始,JavaScript也支持剩余参数,语法允许我们将一个不定量的放到组中。

function foo(...args: (string | number)[]) {

}

foo(123, 321)
foo("abc", 111, "cba")

export {}

函数的重载

什么是函数的重载,我们来看一下下面这个案例

// 需求: 只能将两个数字/两个字符串进行相加
// 案例分析: any实现
// function add(arg1, arg2) {
//   return arg1 + arg2
// }

// add(10, 20)
// add("abc", "cba")
// add({aaa: "aaa"}, 123)


// 1.实现两个函数
// function add1(num1: number, num2: number) {
//   return num1 + num2
// }

// function add2(str1: string, str2: string) {
//   return str1 + str2
// }

// add1(10, 20)
// add2("abc", "cba")


// 2.错误的做法: 联合类型不可以,因为不能确定具体类型
// function add(arg1: number|string, arg2: number|string) {
//   return arg1 + arg2
// }

// 3.TypeScript中函数的重载写法
// 3.1.先编写重载签名
function add(arg1: number, arg2: number): number
function add(arg1: string, arg2: string): string

// 3.2.编写通用的函数实现
function add(arg1: any, arg2: any): any {
  return arg1 + arg2
}

add(10, 20)
add("aaa", "bbb")
// 通用函数不能被调用
// add({name: "Lee"}, "aaa")
// add("aaa", 111)

export {}

从上面的案列我们知道实现函数重载的步骤为:

  1. 编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用
  2. 一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现。
  3. 如上例所示,通用函数是不能被调用的。这句话应该这样理解:通用函数是不能按照不符合重载签名的方式进行调用的。也就是上例中只能单独实现数字或者字符串的相加,不能实现其他类型的运算

函数重载和联合类型的对比

我们现在有一个需求:定义一个函数,可以传入字符串或数组,并且获取它的长度。

// 1.普通的实现
// function getLength(arg) {
//   return arg.length
// }

// 2.函数的重载
// function getLength(arg: string): number
// function getLength(arg: any[]): number
// function getLength(arg) {
//   return arg.length
// }


// 3.联合类型实现(可以使用联合类型实现的情况, 尽量使用联合类型)
// function getLength(arg: string | any[]) {
//   return arg.length
// }

// 4.对象类型实现
function getLength(arg: { length: number }) {
  return arg.length
}


getLength("aaaaa")
getLength(["abc", "cba", "nba"])
// 报错,传入的参数类型不符合定义
getLength({ name: "why", length: 100 })

export {}

从上例我们得出的结论就是:当使用函数重载和联合类型都能实现的情况下,尽量使用联合类型

函数中的this类型

this默认类型, 在没有对TS进行特殊配置的情况下, this是any类型

// 1.对象中的函数中的this
const obj = {
  name: "why",
  studying: function() {
    // 默认情况下, this是any类型
    console.log(this.name.length, "studying")
  }
}

obj.studying()
// obj.studying.call({})

// 2.普通的函数
function foo() {
  console.log(this)
}

export {}

this明确类型

如何指定this的确切类型呢?使用函数的第一个参数类型:

  1. 函数的第一个参数我们可以根据该函数之后被调用的情况,用于声明 this的类型(名词必须叫this)
  2. 在后续调用函数传入参数时, 是从第二个参数开始传递的,this参数会在编译后被抹除。
// 在设置配置选项(编译选项compilerOptions, noImplicitThis设置为true
// 不允许模糊的this存在)

// 1.对象中的函数中的this
const obj = {
  name: "why",
  studying: function(this: {}) {
    // 默认情况下, this是any类型
    console.log(this, "studying")
  }
}

// obj.studying()
obj.studying.call({})

// 2.普通的函数
function foo(this: { name: string }, info: {name: string}) {
  console.log(this, info)
}

foo.call({ name: "James" }, { name: "Kobe" })

export {}

this相关的内置工具

Typescript 提供了一些工具类型来辅助进行常见的类型转换,这些类型全局可用。主要有以下3个:

  1. ThisParameterType :
  • 用于提取一个 函数类型Type 的this (opens new window) 参数类型
  • 如果这个函数类型没有 this 参数则返回 unknown
  1. OmitThisParameter:
  • 用于移除一个函数类型Type的this参数类型 , 并且返回当前的函数类型
  1. ThisType:
  • 这个类型不返回一个转换过的类型,它被用作标记上下文的this类型
function foo(this: { name: string }, info: {name: string}) {
  console.log(this, info)
}

type FooType = typeof foo

// 1.ThisParameterType: 获取FooType类型中this的类型
type FooThisType = ThisParameterType<FooType>

// 2.OmitOmitThisParameter: 删除this参数类型, 剩余的函数类型
type PureFooType = OmitThisParameter<FooType>

// 3.ThisType: 用于绑定一个上下文的this
interface IState {
  name: string
  age: number
}

interface IStore {
  state: IState
  eating: () => void
  running: () => void
}

const store: IStore & ThisType<IState> = {
  state: {
    name: "why",
    age: 18
  },
  eating: function() {
    console.log(this.name)
  },
  running: function() {
    console.log(this.name)
  }
}

store.eating.call(store.state)

export {}

总结

从以上的分析我们可以看出,我们本篇文章所写的大部分知识都与我们掌握的 JS 知识差不多,只是增加了类型检测与约束而已。

posted @ 2024-09-20 09:13  年糕不是黏糕  阅读(18)  评论(0编辑  收藏  举报