二、TypeScript语法细节

2022/9/3 TypeScript联合类型交叉类型type类型别名interface接口声明类型断言as非空类型断言!字面量类型类型缩小函数类型表达式函数调用签名函数构造签名函数的重载this类型及内置工具

# 1、联合类型 |

# 1.1.联合类型

  • TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型。
  • 我们来使用第一种组合类型的方法:联合类型(Union Type)
    • 联合类型是由两个或者多个其他类型组成的类型;
    • 表示可以是这些类型中的任何一个值;
    • 联合类型中的每一个类型被称之为联合成员(union's members);
function printId(id: number | string) {
  console.log("你的id是:",id)
}

printId(10)
printId("sdfdsqa")
1
2
3
4
5
6

# 1.2.使用联合类型

  • 传入给一个联合类型的值是非常简单的:只要保证是联合类型中的某一个类型的值即可
    • 但是我们拿到这个值之后,我们应该如何使用它呢?因为它可能是任何一种类型。
    • 比如我们拿到的值可能是string或者number,我们就不能对其调用string上的一些方法;
  • 那么我们怎么处理这样的问题呢?
    • 我们需要使用缩小(narrow)联合(后续我们还会专门讲解缩小相关的功能);
    • TypeScript可以根据我们缩小的代码结构,推断出更加具体的类型;
function printId(id: number | string) {
  if(typeof id === 'string') {
    console.log("你的id是:",id.toUpperCase())
  }else {
    console.log("你的id是:",id)
  }
}

printId(10)
printId("sdfdsqa")
1
2
3
4
5
6
7
8
9
10

# 2、类型别名(type)

  • 在前面,我们通过在类型注解中编写 对象类型 和 联合类型,但是当我们想要多次在其他地方使用时,就要编写多次。
  • 比如我们可以给对象类型起一个别名:
// 案例一
type ID = number | string
function printId(id: ID) {
  console.log("你的id是:",id)
}

printId(10)
printId("sdfdsqa")

// 案例二
type Point = {
  x: number
  y: number
}

function printPoint(point: Point) {
  console.log(point.x, point.y)
}

printPoint({x:33, y:99})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3、接口声明(interface)

  • 在前面我们通过type可以用来声明一个对象类型:
type Point = { //写法上:有赋值操作
  x: number
  y: number
}
1
2
3
4
  • 对象的另外一种声明类型的方式就是通过接口来声明
interface Point { //写法上:直接声明
  x: number
  y:number
}
1
2
3
4
  • 那么它们有什么区别呢?
    • 类型别名和接口非常相似,在定义对象类型时,大部分时候,你可以任意选择使用。
    • 接口的几乎所有特性都可以在 type 中使用(后续我们还会学习interface的很多特性);

# 4、interface和type区别

  • 我们会发现interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?
    • 如果是定义非对象类型,通常推荐使用type,比如Direction、Alignment、一些Function;
  • 如果是定义对象类型,那么他们是有区别的:
    • interface 可以重复的对某个接口来定义属性和方法
    • 而type定义的是别名,别名是不能重复的

1a11b186185b4592c.png

  • 所以,interface可以为现有的接口提供更多的扩展
    • 接口还有很多其他的用法,我们会在后续详细学习

# 5、交叉类型 &

# 5.1.交叉类型

  • 前面我们学习了联合类型:
    • 联合类型表示多个类型中一个即可
type Alignment = 'left' | 'center' | 'right'
1
  • 还有另外一种类型合并,就是交叉类型(Intersection Types):
    • 交叉类似表示需要满足多个类型的条件;
    • 交叉类型使用 & 符号;
  • 我们来看下面的交叉类型:
    • 表达的含义是number和string要同时满足;
    • 但是有同时满足是一个number又是一个string的值吗?其实是没有的,所以MyType其实是一个never类型
type MyType = number & string
1

# 5.2.交叉类型的应用

  • 所以,在开发中,我们进行交叉时,通常是对对象类型进行交叉的
interface Colorful {
  color: string
}

interface IRun {
  running: () => void
}

type IRunType = {
  eating: () => void
}

type NewType = Colorful & IRun & IRunType // 交叉类型

const obj: NewType = {
  color: 'red',
  running() {
    console.log("running")
  },
  eating() {
    console.log("eating")
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 6、类型断言as

  • 有时候TypeScript无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions)。
    • 比如我们通过 document.querySelector('.list'),TypeScript只知道获取的是 Element | null,但并不知道它具体的类型:
const imgEl = document.querySelector("img")// imgEl的类型: HTMLImageElement | null
const myEl = document.querySelector(".list") //  myEL的类型: Element | null

imgEl.src = "图片地址"  //ts报错提示: 对象可能为 "null"。
myEl.src = "图片地址"  //ts报错提示:对象可能为 "null"。和 类型“Element”上不存在属性“src”。
//以上两种情况如果我们给他们设置src图片地址,会报错


//如果我们想要设置src地址,需要类型断言as (类型转换) 为 HTMLImageElement(即断言为具体的类型:img元素对象类型)
const imgEl1 = document.querySelector("img") as HTMLImageElement
imgEl1.src = "图片地址"

1
2
3
4
5
6
7
8
9
10
11
12
  • TypeScript只允许类型断言转换为 更具体 或者 不太具体 的类型版本,此规则可防止不可能的强制转换:
const name = 'kobe' as number// ts报错提示:类型 "string" 到类型 "number" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"。
const name1 = ('kobe' as unknown) as number
1
2

# 7、非空类型断言!

  • 当我们编写下面的代码时,在执行ts的编译阶段会报错:
    • 这是因为传入的message有可能是为undefined的,这个时候是不能执行方法的;
function printMessage(message?: string) {//这里函数的message参数类型 使用的是可选类型;所以message的类型为:string | undefined
  console.log(message?.toUpperCase())// 正确写法: 使用可选链: 表示message有值才调用后面的方法;没有值则不调用 直接打印:undefined
  console.log(message.toUpperCase())// ts报错提示:对象可能为“未定义”。
}
1
2
3
4
  • 但是,我们确定传入的参数是有值的,这个时候我们可以使用非空类型断言
    • 非空断言使用的是 ! ,表示可以确定某个标识符是有值的跳过ts在编译阶段对它的检测
function printMessage(message?: string) {//这里函数的message参数类型 使用的是可选类型;所以message的类型为:string | undefined
  console.log(message!.toUpperCase())// 正确写法:使用非空类型断言: 表示确定message标识符是有值的
}
1
2
3

# 8、字面量类型

# 8.1.字面量类型

  • 除了前面我们所讲过的类型之外,也可以使用字面量类型(literal types):
let message: "Hello" = "Hello"

message = 'Hello World' //ts报错提示:不能将类型“"Hello World"”分配给类型“"Hello"”。
1
2
3
  • 那么这样做有什么意义呢?
    • 默认情况下这么做是没有太大的意义的,但是我们可以将多个类型联合在一起;
type Alignment = 'left' | 'right' | 'center'

function changeAlign(align: Alignment) {
  console.log("修改方向:", align)
}

changeAlign("left")
changeAlign("right")
changeAlign("center")
changeAlign("123")// ts报错提示:类型“"123"”的参数不能赋给类型“Alignment”的参数。
1
2
3
4
5
6
7
8
9
10

# 8.2.字面量推理

  • 我们来看下面的代码:
const info = {
  url: "http://coder.org/abc",
  method: "GET"
}

function request(url: string, method: "GET" | "POST") {
  console.log(url, method)
}

request(info.url, info.method) // ts报错提示(info.method):类型“string”的参数不能赋给类型“"GET" | "POST"”的参数。
1
2
3
4
5
6
7
8
9
10
  • 这是因为我们的对象在进行字面量推理的时候,info其实是一个 {url: string, method: string},所以我们没办法将一个 string赋值给一个 字面量 类型 【有如下两种方式,来解决这个问题】

  • 方式一:类型断言为 "GET":as "GET"

//方式一:
const info = {
  url: "http://coder.org/abc",
  method: "GET"
}

function request(url: string, method: "GET" | "POST") {
  console.log(url, method)
}

request(info.url, info.method as "GET") 
1
2
3
4
5
6
7
8
9
10
11
  • **方式二:as const 也是类型断言的一种:**这被称为const断言。const断言告诉编译器为表达式推断出它能推断出的最窄或最特定的类型。如果不使用它,编译器将使用其默认类型推断行为,这可能会导致更广泛或更一般的类型。
//方式二:
const info = {
  url: "http://coder.org/abc",
  method: "GET"
} as const

function request(url: string, method: "GET" | "POST") {
  console.log(url, method)
}

request(info.url, info.method)
request(info.url, "POST")
1
2
3
4
5
6
7
8
9
10
11
12

# 9、类型缩小

# 9.1. 类型缩小

  • 什么是类型缩小呢?

    • 类型缩小的英文是 Type Narrowing(也有人翻译成类型收窄);
    • 我们可以通过类似于 typeof padding === "number" 的判断语句,来改变TypeScript的执行路径;
    • 在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为 缩小( Narrowing );
    • 而我们编写的 typeof padding === "number 可以称之为 类型保护(type guards);
  • 常见的类型保护有如下几种:

    1. typeof
    2. 平等缩小(比如===、!==)
    3. instanceof
    4. in操作符
    5. 等等...

# 9.2.typeof

  • 在 TypeScript 中,检查返回的值typeof是一种类型保护:
    • 因为 TypeScript 对如何typeof操作不同的值进行编码。
type ID = string | number

function printId(id: ID) {
  if(typeof id === 'string') {
    console.log(id.toUpperCase())
  }else {
    console.log(id)
  }
}
1
2
3
4
5
6
7
8
9

# 9.3.平等缩小

  • 我们可以使用Switch或者相等的一些运算符来表达相等性(比如===, !==, ==, and != ):
type Direction = 'left' | 'center' | 'right'

function turnDirection(direction: Direction) {
  switch(direction) {
    case 'left':
      console.log("调用left方法操作")
      break
    case 'center':
      console.log("调用center方法操作")
      break
    case 'right':
      console.log("调用right方法操作")
      break
    default:
      console.log("调用默认方法")
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 9.4.instanceof

  • JavaScript 有一个运算符来检查一个值是否是另一个值的“实例”:
function printValue(date: Date | string) {
  if(date instanceof Date) {
    console.log(date.getTime())
    console.log(date.toLocaleString())
  } else{
    console.log(date)
  }
}

printValue(new Date('2022-10-3')) // 1664726400000   2022/10/3 00:00:00
printValue('my name is kobe') //my name is kobe
1
2
3
4
5
6
7
8
9
10
11

# 9.5.in操作符

  • Javascript 有一个运算符,用于确定对象是否具有带名称的属性:in运算符
    • 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true;
type Fish = {
  swim: () => void
}

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

function move(animal: Fish | Dog) {
  if('swim' in animal) {
    animal.swim()
  }
  if('run' in animal){
    animal.run()
  }
}

const obj = {
  swim(){
    console.log("swim")
  },
  run() {
    console.log("run")
  }
}
move(obj)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 10、TypeScript函数类型

# 10.1.TypeScript函数类型(函数类型的表达式)

  • 在JavaScript开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进行传递)。
  • 那么在使用函数的过程中(函数作为参数传入另一个函数的时候),函数是否也可以有自己的类型呢?
  • 我们可以编写函数类型的表达式(Function Type Expressions),来表示函数类型
type CalcFunc = (num1:number, num2:number) => void //函数类型表达式

function calc(fn: CalcFunc) {
  console.log("name:",fn.name)
  console.log(fn(20,30))
}

function sum(num1:number, num2:number) {
  return num1 + num2
}
function mul(num1:number, num2:number) {
  return num1 * num2
}

calc(sum)
calc(mul)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 给函数自身添加属性的情况:如给函数添加age属性

2.png

# 10.2.TypeScript函数类型解析

  • 在上面的语法中 (num1: number, num2: number) => void,代表的就是一个函数类型:
    • 接收两个参数的函数:num1和num2,并且都是number类型;
    • 并且这个函数是没有返回值的,所以是void;
  • 注意:在某些语言中,可能参数名称num1和num2是可以省略,但是TypeScript是不可以的:
    • 官方描述:Note that parameter name is required. The function type (string) => void means'a function with a parameter named string of type any 【请注意,参数名是必需的。函数类型 (string) = > void 表示“一个带有名为 string 的参数且该参数的类型为 any 的函数”】

# 10.3.调用签名(Call Signatures)

  • 在 JavaScript 中,函数除了可以被调用,自己也是可以有属性值的。
    • 然而前面讲到的函数类型表达式并不能支持声明属性;
    • 如果我们想描述一个带有属性的函数,我们可以在一个对象类型中写一个调用签名(call signature);
  • 注意这个语法跟函数类型表达式稍有不同,在参数列表和返回的类型之间用的是 : 而不是 =>
interface ICalcFn {//调用签名
  age:number
  name: string
  (num1: number,num2: number): void
}

function calc(calcFn: ICalcFn) {
  console.log(calcFn.name,calcFn.age)
  calcFn(10,20)
}
function sum(num1:number,num2:number){
  console.log(num1 + num2)
}
sum.age = 111
calc(sum)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 10.4.构造签名 (Construct Signatures)

  • JavaScript 函数也可以使用 new 操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数(constructors),因为他们会产生一个新对象。
  • 你可以写一个构造签名( Construct Signatures ),方法是在调用签名前面加一个 new 关键词;
interface Iperson { //构造签名
  new (name: string,age: number): Person
}

function factory(ctor: Iperson) {
  return new ctor('kobe',45)
}

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

console.log(factory(Person)) //Person { name: 'kobe', age: 45 }

const p = new Person("james",38)
console.log(p) //Person { name: 'james', age: 38 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 10.5.参数的可选类型

  • 我们可以指定某个参数是可选的:
function bar(x:number,y?:number) {//x参数类型为:number; y参数的类型为:number | undefined
  console.log(x,y)
}
1
2
3
  • 这个时候这个参数y依然是有类型的,它是什么类型呢? number | undefined
    • 官网描述:Although the parameter is specified as type number. the x parameter will actually have the type number | undefined because unspecified parameter in JavaScript get the value undefined 【尽管参数被指定类型为 number。X 参数实际上会有类型 number | undefined,因为 JavaScript 中未指定的参数会得到 undefined 的值】
  • 另外可选类型需要在必传参数的后面:
function bar(x?:number,y:number) {//ts报错提示: 必选参数(y)不能位于可选参数(x)后。
  console.log(x,y)
}
1
2
3

# 10.6.默认参数

  • 从ES6开始,JavaScript是支持默认参数的,TypeScript也是支持默认参数的:
// 下面foo函数的类型为: function foo(x: number, y?: number): void
function foo(x: number, y: number = 9) {  
  console.log(x,y)
}
foo(10)
1
2
3
4
5

# 10.7.剩余参数

  • 从ES6开始,JavaScript也支持剩余参数,剩余参数语法允许我们将一个不定数量的参数放到一个数组中。
    • 注意:rest(剩余) 参数必须是函数参数列表中的最后一个参数。
function sum (...nums: number[]) {
  let total = 0
  nums.forEach(item => total += item)
  return total
}

const result = sum(10,20,30,40)
console.log(result)
1
2
3
4
5
6
7
8

# 10.8.函数的重载(了解)

  • 在TypeScript中,如果我们编写了一个add函数,希望可以对字符串和数字类型进行相加,应该如何编写呢?
  • 我们可能会这样来编写,但是其实是错误的:
function sum(a1: number | string, a2: number | string): number | string {
  return a1 + a2  //ts错误提示: 运算符“+”不能应用于类型“string | number”和“string | number”。
}
1
2
3
  • 那么这个代码应该如何去编写呢?
    • 在TypeScript中,我们可以去编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用;
    • 一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现;

# 10.9.sum函数的重载

  • 比如我们对sum函数进行重构:
    • 在我们调用sum的时候,它会根据我们传入的参数类型来决定执行函数体时,到底执行哪一个函数的重载签名;
    • 注意:函数重载签名,应该写在函数声明之前
function sum(a1: number, a2: number): number
function sum(a1: string, a2: string): string
function sum(a1: any, a2: any): any {// 实现体的函数
  return a1 + a2  
}

console.log(sum(1,2)) //3
console.log(sum('james','nba')) //jamesnba
1
2
3
4
5
6
7
8
  • 但是注意,有实现体的函数,是不能直接被调用的:

3.png

# 10.10.联合类型和重载

  • 我们现在有一个需求:定义一个函数,可以传入字符串或者数组,获取它们的长度。
  • 这里有两种实现方案:
    • 方案一:使用联合类型来实现;
    • 方案二:实现函数重载来实现;
//方案一:联合类型
function getLength(o: string | any[]) {
  console.log(o.length)
}

getLength('james') //5
getLength([1,3,4,6,'3412',34,true,{},520])//9


//方案二:函数重载
function getLength(o: string):void
function getLength(o: any[]):void
function getLength(o:any) {
  console.log(o.length)
}

getLength('james') //5
getLength([1,3,4,6,'3412',34,true,{},520])//9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 在开发中我们选择使用哪一种呢?
    • 在可能的情况下,尽量选择使用联合类型来实现

# 11、TypeScript的this类型

# 11.1.可推导的this类型

  • this是JavaScript中一个比较难以理解和把握的知识点:
  • 当然在目前的Vue3和React开发中你不一定会使用到this:
    • Vue3的Composition API中很少见到this,React的Hooks开发中也很少见到this了;
  • 但是我们还是简单掌握一些TypeScript中的this,TypeScript是如何处理this呢?我们先来看两个例子:
const obj = {
  name: 'obj',
  foo() {
    console.log(this.name)  //this的类型为: any
  }
}

obj.foo()

function foo1() {
  console.log(this)//this的类型为: any
}

foo1()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 上面的代码默认情况下是可以正常运行的,也就是TypeScript在编译时,认为我们的this是可以正确去使用的:
    • 这是因为在没有指定this的情况,this默认情况下是any类型的;(隐式)

# 11.2.this的编译选项

  • VSCode在检测我们的TypeScript代码时,默认情况下运行不确定的this按照any类型去使用。
    • 但是我们可以创建一个tsconfig.json文件,并且在其中告知VSCodethis必须明确执行(不能是隐式的);
    • noImplicitThis (无隐式this)

474a968d72bcc237a.png

  • 在设置了noImplicitThis为true时, TypeScript会根据上下文推导this,但是在不能正确推导时,就会报错,需要我们明确的指定this类型。
const obj = {
  name: 'obj',
  foo() {
    console.log(this.name)  //【可正确推导类型】this的类型推导为为: this: {name: string; foo(): void;}
  }
}

obj.foo()

function foo1() {
  console.log(this)//【不能正确推导类型】ts报错提示:"this" 隐式具有类型 "any",因为它没有类型注释。
}
foo1()
1
2
3
4
5
6
7
8
9
10
11
12
13

# 11.3.指定this的类型

  • 在开启noImplicitThis的情况下,我们必须指定this的类型。
  • 如何指定呢?函数的第一个参数类型
    • 函数的第一个参数我们可以根据该函数之后被调用的情况,用于声明this的类型(名词必须叫this);
    • 在后续调用函数传入参数时,从第二个参数开始传递的,this参数会在编译后被抹除
function foo(this: {name: string}) {
  console.log(this)
}

foo.call({ name: 'lyk' })
1
2
3
4
5

# 11.4.this相关的内置工具

  • Typescript 提供了一些工具类型来辅助进行常见的类型转换,这些类型全局可用。
  • ThisParameterType:
    • 用于提取一个函数类型Type的this (opens new window)参数类型;
    • 如果这个函数类型没有this参数返回unknown;
// 1.ThisParameterType<fn> 用于提取一个函数类型Type的this (opens new window)参数类型
function foo(this: {name: string, age: number}) {
  console.log(this.name,this.age)
}
//获取一个函数中this的类型
type fooThisType = ThisParameterType<typeof foo>
//获取到foo函数的this类型为:
// type fooThisType = {
//   name: string;
//   age: number;
// }


// 2.如果这个函数类型没有this参数返回unknown;
function bar() {
  console.log('bar')
}
type barThisType = ThisParameterType<typeof bar>
//获取到foo函数的this类型为:
// type barThisType = unknown
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • OmitThisParameter:
    • 用于移除一个函数类型Type的this参数类型, 并且返回当前的函数类型
function foo(this: {name: string, age: number}) {
  console.log(this.name,this.age)
}
//用于移除一个函数类型Type的this参数类型, 并且返回当前的函数类型
type FnType = OmitThisParameter<typeof foo> // 当前的函数类型 :type FnType = () => void
1
2
3
4
5

# 11.5.this相关的内置工具 - ThisType

  • 这个类型不返回一个转换过的类型,它被用作标记一个上下文的this类型。(官方文档)
    • 事实上官方文档的不管是解释,还是案例都没有说明出来ThisType类型的作用;
  • 我这里用另外一个例子来给大家进行说明:(pinia内部this实现)
interface IState {
  name: string
  age: number
}

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


const info: IData = {
  state: {
    name: 'kobe',
    age: 45
  },
  running() {//根据类型推导;这里的this类型为: this: IData
    console.log(this.name,'running')//ts报错提示:类型“IData”上不存在属性“name”。
  },
  eating() {//根据类型推导;这里的this类型为: this: IData
    console.log(this.name,'eating')//ts报错提示:类型“IData”上不存在属性“name”。
  }
}

const info1: IData = {
  state: {
    name: 'kobe',
    age: 45
  },
  running() {//根据类型推导;这里的this类型为: this: IData   
    //IData.state的类型为:IData.state: IState;  所以可以正常拿到info1中state的name属性值
    console.log(this.state.name,'running')
  },
  eating() {//根据类型推导;这里的this类型为: this: IData
    console.log(this.state.name,'eating')
  }
}
info1.eating()//kobe eating


//使用TypeScript的this类型的内置工具 ThisType:指定info2中的方法的this类型为:IState
const info2: IData & ThisType<IState> = {
  state: {
    name: 'kobe',
    age: 45
  },
  running() {
    console.log(this.name,'running')//this的类型为:this: IState
  },
  eating() {
    console.log(this.name,'eating')//this的类型为:this: IState
  }
}

//给info2中的方法绑定this
info2.running.call(info2.state)//kobe running
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
最后更新时间: 2022/10/11, 11:33:12
彩虹
周杰伦