likes
comments
collection
share

超级详细的typescript入坑指南

作者站长头像
站长
· 阅读数 62

前言

一:为什么要学习ts

我们先来看下面这个js代码:

function getUserName() {
 if (Math.random() < 0.5) {
  return "zhang san";
 }
 return 404;
}
let myname = getUsername();
mynema = myname
 .split(" ")
 .filter((it) => it)
 .map((it) => it[0].touppercase() + it.subStr(1))
 .join(" ");

代码的内容很简单,是一个将名字首字母大写的功能。但是我们不难看出这段代码有很明显的几处错误:

1. 使用了不存在的变量、函数或成员。在上图中,getUserName方法在调用时写成了getUsername,声明的变量myname在使用时写成了mynematoUpperCase方法写成了touppercasesubstr方法写成了subStr

2. 类型错误,把一个不确定的类型当做一个确定的类型处理。 在上图中,getUserName方法返回的数据,可能会string类型,也可能为number类型,然而在使用时,完全把getUserName方法返回的数据当做string来处理,就会造成报错;

此外,我们在开发过程中很容易使用nullundefined中的成员。 如下图中所示: 超级详细的typescript入坑指南

当然,我们不会直接书写这样的代码。但是这个错误是很常见的,因为我们开发过程中使用的对象,大多可能是调用另一个函数或者请求后台接口获取的,甚至有可能开发时数据不够完善的情况下,一直没有暴露出这个问题,直到项目上线之后才发现这种错误,这无疑增加了修改bug的时间,且降低了项目的代码质量。

js语言创建之初,仅仅是为了解决一些简单的业务场景,如图片文字的展示、幻灯片播放,js语言本身的特性(即弱类型解释型)不能在开发过程中及时提示这些错误,需要编译完成之后在浏览器才能看到报错信息。

二:ts的特点

ts是什么

引用ts官网中的描述:“typeScriptjs超集,是一个 可选的静态的类型系统。”下面解释一下这几个关键词的含义:

超集:如整数和正整数的关系,即ts包含js的所有功能,且比js多一些功能。

超级详细的typescript入坑指南

ts比js多出来的部分,就是类型系统,对代码中所有的标识符(变量、函数、参数、返回值)进行类型检查。

可选的:类型系统可以用,也可以不用。

静态的:无论是浏览器环境还是node环境,无法直接识别ts代码,需要使用ts编译器tsc转换为es。静态的类型检查发生的时间在编译的时候,而非运行时。

ts意外的惊喜

原本使用ts的目的仅仅是为了使用类型检查减少一些js开发中的错误,但是发现有了类型检查之后,无形中增强了面向对象的开发。js也有类和对象,本身就支持面向对象开发,但是由于缺少类型检查,在面向对象开发过程中会遇到很多问题。一般在开发过程中,除了使用react进行开发外,一般很少使用类进行开发。一方面是因为多数的前端开发工作者不习惯面向对象的思维模式,此外js缺少类型检查,使得面向对象开发的诸多场景会出现问题。使用ts后,可以编写出完善的面向对象代码。

三:ts的使用

在了解ts语法之前,我们先搭建一个学习ts的开发环境。

安装typescript:npm i -g typescript ,建议使用全局安装,这样可以更方便的使用tsc。

安装好之后,创建一个index.ts文件,写入下面的代码

let say:string = 'hello'

执行tsc index.ts,就会生成一个index.js文件,内容如下:

let say = 'hello'

此时原本的index.ts文件就会出现如下报错: 超级详细的typescript入坑指南

造成错误的原因:默认情况下,ts会自动作出以下几种假设:

  1. 假设当前执行的环境是浏览器的dom环境;
  2. 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行;
  3. 编译的目标代码是es3。

解决上述几种问题的方法:

  1. 在使用tsc命令时,加上选项参数(不实用,不推荐);
  2. 使用ts的配置文件,更改编译选项。

如何添加ts配置文件:

  1. 直接在资源管理器中添加一个名为tsconfig.json的新文件。
  2. 使用命令tsc --init 生成配置文件

使用命令生成的配置文件有很多属性,并且默认会在后面标注属性的含义和使用方式,但并不是所有的属性都需要用到,可以先把所有的属性删除。

{
 "compilerOptions": { // 编译选项
  "target": "es2016", // 编译目标使用的js标准,如:es3、es5、es2015,默认是es3。注意:es6及之后发行的版本,都使用年份。如:es2015、es2017
  "module": "commonjs", // 编译目标使用的模块化标准,如commonjs、es6
  "lib": ["es2016"] // ts编译的环境,默认有"dom"环境,即在浏览器环境下编译。
   // 去掉"dom"环境后,浏览器自带的document等方法就不生效了,同时console等方法也不生效了,然而ts没有自带的node环境,此时应该引入一个@types/node插件。
   // @types是一个ts官方的类型库,其中包含了很多对js代码的类型描述。
   // 使用这个库是由于js在引用一些第三方库时(jquery等),这些第三方库没有使用ts,而类型检查又需要检查这些第三方库,就可以在@types里面找一下有没有对应的类型系统。
   // 比如jQuery,没有类型检查,在ts项目中想要使用jQuery,就可以安装@types/jquery,进行类型定义。
   // 使用 npm i -D @types/node
 }
}

相关配置完成后,ts文件中就已经不报错了。

加了配置文件之后,使用tsc进行编译时,就不能加上文件名了,如果加了文件名,编译时会忽略掉配置文件。在编译时直接使用tsc进行编译就行。

新建一个src文件夹,将index.ts文件复制到src文件夹下。执行tsc命令,默认情况下,使用tsc进行编译的是整个工程下所有的代码,即src目录下和根目录下的ts文件都会被编译。但是我们并不需要执行整个工程的代码,而是只需要编译src目录下的文件,只需在配置文件中加一个属性:include(与compilerOptions同级),表示要编译哪个文件夹下的代码。

{
 "include": ["./src"]
}

如果只需要编译src下的某一个文件,则使用files(与compilerOptions同级)进行配置

{
 "files": ["./src/index.ts"]
}

但是此时编译的结果是和ts文件是放在同一个文件夹下的,如果想要配置编译后的js文件的位置,就使用outDircompilerOptions下级)进行配置

{
 "compilerOptions": {
  "target": "es2016",
  "module": "commonjs",
  "lib": ["es2016"],
  "outDir": "./dist"
 }
}

使用第三方库可以减少操作: ts-node 将ts在内存中完成编译,同时完成运行不会产生js文件

执行命令:ts-node src/index.ts

nodemon 监测文件变化,保存文件后会自动编译和运行

执行命令:nodemon --exec ts-node src/index.ts

此时nodemon会监测整个项目,如果需要设置只监测ts文件

执行命令:nodemon -e ts --exec ts-node src/index.ts

如果只需要监测src下的文件

执行命令:nodemon --watch src -e ts --exec ts-node src/index.ts

将这个命令配置到package.json的脚本中,后续就可以使用命令开启一个开发环境,再使用tsc打包到disc文件夹中。

{
  "scripts": {
    "serve": "nodemon --watch src -e ts --exec ts-node src/index.ts",
    "build": "tsc"
  },
  "dependencies": {
    "@types/node": "^18.15.3",
    "nodemon": "^2.0.21",
    "ts-node": "^10.9.1"
  }
}

四:ts语法

ts类型约束哪些内容:变量、函数的参数、函数的返回值

约束方法: 在变量、函数的参数、函数的返回值后面加上 : + 类型

例如:

变量约束

let say:string = 'hello'

这样在给say赋值的时候,如果赋值为非string的值,就会报错

超级详细的typescript入坑指南

函数的参数和返回值

function sum(a: number, b: number): number {
  return a + b
}

这样在调用函数的时候,传入了非number的参数就会报错

超级详细的typescript入坑指南

或者将这个函数的返回值赋值给一个非number的变量

超级详细的typescript入坑指南

函数的返回值,即使不手动书写,ts也能自动监测到

超级详细的typescript入坑指南

如何知道ts什么时候能自动检测什么时候监测不到呢

超级详细的typescript入坑指南

下面出现三个点的情况下,可以认为是一个警告,表示无法检测到类型,会默认标记为any类型,ts不会对any类型进行任何类型检查。

在js中,由于没有类型检查,一个函数甚至可以赋值为一个字符串,而这种错误在ts中很容易被发现,如果命名重复,需要修改某个变量或函数名,而这个变量或函数已经调用多次,可以点击F2,直接修改名字,这样在项目中所有调用这个变量或函数的地方都会更改为新的名字。即使写在别的文件中,只要使用了模块化,也可以修改。

超级详细的typescript入坑指南

点击F12可以直接转到定义

ts基础类型

任意类型any

声明为any的变量可以赋值为任何类型,将不会对any类型的数据进行类型检查,所以any类型的数据可以赋值给任何一个类型

let a: any = 123
a = '123'

数字类型number

双精度64位浮点值,可以用来表示整数和分数。

let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744;  // 八进制
let decLiteral: number = 6;  // 十进制
let hexLiteral: number = 0xf00d;  // 十六进制

字符串类型string

let a: string = '123'

布尔类型boolean

表示逻辑值:true和false

let a: boolean = true

数组类型

在声明数组时

let arr: number[] = [1, 2]
let arr: Array<number> = [1, 2]

元组

用来表示 **固定长度 **的数组并且每一项的类型都确定,对应的位置类型必须相同

let x: [string, number];
x = ['Runoob', 1];  // 运行正常
x = [1, 'Runoob'];  // 报错
console.log(x[0]);  // 输出 Runoob

对象object

表示对象类型,智能约束这是个对象类型,但是没法精准的约束对象里面的每一项,所以并不常用

let obj: object = {}

void

用于标识函数没有任何返回值

function sum (a: number, b: number) {
  console.log(a + b)
}

null

表示对象缺失,null和undefined是其他除了never以外的类型的子类型,即undefined和null可以赋值给其他任何类型

let a: null = null
let b: string = a

undefined

用于初始化变量一个未定义的值

// let a: undefined = undefined
// let b: string = a

undefinednullnever赋值给其他类型的情况下,这些值在调用一些方法的时候还是会报错,可以在配置文件的compilerOptions里面加上属性strictNullChecks: true,表示更加严格的校验,这样undefinednull就不能赋值给其他类型了。

never

表示函数永远不会结束或永远不存在的变量,是任何类型(包括nullundefined)的子类型,例如函数抛出一个错误或进入无限循环

function sum (a: number, b: number): never {
  while (true) {
    console.log(a + b)
  }
}

联合类型

let num: string | number | undefined

字面量

表示使用一个值进行约束,而不是一个类型

let a: 'A’' // 表示a永远只能赋值为A
let sex: '男' | '女' // 强力约束性别永远只能取男或女

在给数组规定类型时,一定要规定数组的每一位的类型,如果写为 let arr: [] 表示声明一个数组永远只能为空数组

在给对象进行约束时,就可以使用字面量

let obj: {
 name: string;
 age: number;
 sex: "男" | "女";
};

可选参数

可以在某些参数名后面加上问号,表示该参数可以不用传递

function sum (a: number, b: number, c?: number) {}

此时参数c的类型被识别为c: number | undefined

可选参数的位置必须在末尾

默认参数

function sum (a: number, b: number, c: number = 0) {}

此时参数c的类型被识别为c?: number

在给函数的参数规定类型时,参数可能为多种情况,而不同情况可能会造成错误,可以在函数实现之前,对函数调用的多种情况进行声明

超级详细的typescript入坑指南

ts扩展类型

类型别名

例如上述对象字面量中,要声明一个对象数组

type gender = '男' | '女'
type obj = {
 name: string
 age: number
 sex: gender
}
let arr: obj[]

表示这个数组中的对象的内容必须是这个对象字面量的内容

枚举enum

约束某个变量的取值范围

enum gender {
    man = '男',
    woman = '女'
}
enum level {
    level1 = 1,
    level2,
    level3
}
console.log(level.level2)

字面量结合联合类型,也能实现枚举的效果(例如上述案例中,性别的枚举),但是为什么还需要枚举呢?

  1. 使用字面量结合联合类型,需要重复书写代码(可以使用类型别名解决)
  2. 逻辑名称和真实的值会产生混淆,导致修改真实值的时候,产生大量修改(使用enum可以直接修改类型的真实值,逻辑名称需要修改时,可以点击F2进行重构)
  3. 字面量类型不会进入编译结果,enum编译后,表现为对象

超级详细的typescript入坑指南

编译为

超级详细的typescript入坑指南

枚举的字段值,可以是字符串也可以是数字,字段值为数字的枚举叫做数字枚举,数字枚举的值会自动递增

超级详细的typescript入坑指南

如果第一个也不赋值为1,默认从0开始

接口interface

用于约束类、对象、函数的契约(标准)

  1. 接口定义对象
interface User {
  name: string
  age: number
}
let user: User = {
  name: 'zhangsan',
  age: 28
}

目前接口和类型别名在约束对象时的区别不大,最大的区别在于接口可以约束类。一般情况下约束对象建议使用interface。interface的内容不会出现在编译结果中。

  1. 接口约束函数 约束对象中的方法:
interface User {
  name: string
  age: number
  sayHello: () => void
}
let user: User = {
  name: 'zhangsan',
  age: 28,
  sayHello: () => {
    console.log('hello')
  }
}

或:

interface User {
  name: string
  age: number
  sayHello(): void
}
 
let user: User = {
  name: 'zhangsan',
  age: 28,
  sayHello: () => {
    console.log('hello')
  }
}

直接约束函数,以下方函数举例:

function sum(numbers: number[], callback: (num: number) => boolean) {
  let s = 0
  numbers.forEach(item => {
    if (callback(item)) {
      s += item
    }
  })
  return s
}
console.log(sum([1, 2, 3], num => num % 2 !== 0))

使用类型别名也能将函数的类型提取出来:

type Condition = (num: number) => boolean
function sum(numbers: number[], callback: Condition) {
  let s = 0
  numbers.forEach(item => {
    if (callback(item)) {
      s += item
    }
  })
  return s
}
console.log(sum([1, 2, 3], num => num % 2 !== 0))

使用interface约束:

interface Condition {
  (num: number): boolean
}
function sum(numbers: number[], callback: Condition) {
  let s = 0
  numbers.forEach(item => {
   if (callback(item)) {
     s += item
   }
  })
  return s
}
console.log(sum([1, 2, 3], num => num % 2 !== 0))

接口可以继承,可以让一个接口拥有另一个接口的所有成员

超级详细的typescript入坑指南

也可以同时继承多个接口:

超级详细的typescript入坑指南

使用类型别名可以使用交叉类型“&”实现同样的效果

超级详细的typescript入坑指南

但是在接口中不能定义一个重复的类型,而类型别名是可以的重复定义的

超级详细的typescript入坑指南

如果两次定义的类型不一样,最终的类型为两个类型的合并类型,例如:“number & string”,即这个值拥有string和number的所有属性,但是这样做没法赋值,毫无意义。

readonly修饰符:修饰的目标是只读的

interface User {
  id: string
  name: string
  age: number
}
 
let user: User = {
  id: '123',
  name: 'zhangsan',
  age: 28
}
user.id = '456'

上述方法中,假设我们并不希望对id属性进行重新赋值,但有时因为方法过于复杂,会造成误操作。此时我们可以在id的类型前面加上修饰符。只读修饰符不会进入编译结果。

超级详细的typescript入坑指南

数组设置只读修饰符,这个数组依然可以重新赋值。

超级详细的typescript入坑指南

超级详细的typescript入坑指南

上面的方法表示这个变量是一个只读的数组,即不能使用push、splice等修改数据的方法,也不能单独改变数组中的某个成员

放到对象中就不能重新赋值了

超级详细的typescript入坑指南

但是可以使用push、splice等方法修改数组里面的值

超级详细的typescript入坑指南

如果希望数组不能重新赋值的同时,不能修改里面的内容:

超级详细的typescript入坑指南

类型的兼容性

对象的兼容性:

ts在判断类型时,并不是严格的要求对象中的属性类型一一对应,只要当前需要使用的某几位符合要求就可以。例如调用后台的接口请求过来的用户信息,可能包含很多内容,但是前端在使用时并不需要那么多内容,那么只需要定义代码中需要使用的属性的类型就行。

interface UserInfo {
  name: string
  age: number
}
let userInfo = {
  name: 'zhangsan',
  age: 28,
  sex: 'man'
}
let user: UserInfo = userInfo

但是不能直接赋值:

超级详细的typescript入坑指南

函数的兼容性:

interface Condition {
  (num: number, index: number): boolean
}
 
function sum(numbers: number[], callback: Condition) {
  let s = 0
  numbers.forEach((item, index) => {
    if (callback(item, index)) {
      s += item
    }
  })
  return s
}
console.log(sum([1, 2, 3], num => num % 2 !== 0))

上面的代码中,Condition需要接收两个参数,然而在调用时,只传了一个参数,但是并不会报错,需要使用第二个参数时,直接使用就可以

console.log(sum([1, 2, 3], (num, index) => index % 2 !== 0))

类型断言

let value: any
value = 'abc'
console.log((<string>value).length)
console.log((value as string).length)
(value as string[]).push('hi')

联合类型可以被断言为其中一个类型 父类型可以被断言为子类型 任何类型都可以被断言为any类型 any类型可以被断言为一个子类型

类class

超级详细的typescript入坑指南

上述代码直接报错,因为ts要求一个类中所有的属性及属性的类型必须是设定好的,所以类中的属性必须在属性列表中设定,而不是在构造函数中动态添加。属性列表不会出现在编译结果中。

超级详细的typescript入坑指南

规定好一个类中的所有属性后,也不能动态的添加属性了

超级详细的typescript入坑指南

如果想要避免忘记在构造函数中设置属性的值,就在tsconfig.json文件中配置:strictPropertyInitialization: true

设置属性的默认值:

class User {
  name: string
  age: number
  sex: '男' | '女'

  constructor(name: string, age: number, sex: '男' | '女' = '男') {
    this.name = name
    this.age = age
    this.sex = sex
  }
}
const user = new User('zhangsan', 28)

也可以:

class User {
  name: string
  age: number
  sex: '男' | '女' = '男'

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}
const user = new User('zhangsan', 28)
user.sex = '女'

可选的属性:

class User {
  name: string
  age: number
  sex: '男' | '女' = '男'
  pId: string | undefined
  
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}
const user = new User('zhangsan', 28)
user.pId = '123123'

也可以:

class User {
  name: string
  age: number
  sex: '男' | '女' = '男'
  pId?: string
 
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}
const user = new User('zhangsan', 28)
user.pId = '123123'

只读的属性:

超级详细的typescript入坑指南

不能在外部使用的属性:

超级详细的typescript入坑指南

访问修饰符可以控制类中的某个成员的访问权限,不会出现在编译结果中

public:默认的,公开的权限,所有的代码均可访问

private:私有的,只有在类内部可以使用

protected:受保护的

如果某个属性,是在构造函数中直接传进来,然后直接赋值的(例如上述案例中的nameage),可以简写为:

class User {
  sex: '男' | '女' = '男'
  pId?: string
  readonly id: number
  private auth: string = 'view'

  constructor(public name: string, public age: number) {
    this.id = Math.random()
  }
}
const user = new User('zhangsan', 28)

编译结果为:

class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.sex = '男';
    this.id = Math.random();
  }
}
const user = new User('zhangsan', 28);

泛型

function take (arr, n) {
  if (n >= arr.length) {
    return arr
  }
  const newArr = []
  for (let i = 0; i < n; i++) {
    newArr.push(arr[i])
  }
  return newArr
}
const newArr = take([1, 2, 3, 4, 5], 2)
console.log(newArr)

上述代码可以正常运行,但是没有添加任何ts代码,作为ts开发来说不够完善。但是给take函数的arr参数进行定义时,无法确定它的每一位是什么类型的。如果使用any类型,无法保证传入的arr参数、内部的newArr、函数的返回值表示同一个类型。假设我们需要遍历newData的内容,在书写代码时我们将无法确定newData的每一位是什么类型,相当于丢失了一些有关联的类型信息。

function take (arr: any[], n: number): any[] {
  if (n >= arr.length) {
    return arr
  }
  const newArr: any = []
  for (let i = 0; i < n; i++) {
    newArr.push(arr[i])
  }
  return newArr
}
const newData = take([1, 2, 3, 4, 5], 2)
newData.forEach((item: string) => {})
console.log(newData)

泛型:是指附属于函数、类、接口、类型别名之上的类型。使用时在函数、类、接口、类型别名的名称之后加上<泛型名>。不会出现在编译结果中。

function take<T> (arr: T[], n: number): T[] {
  if (n >= arr.length) {
    return arr
  }
  const newArr: T[] = []
  for (let i = 0; i < n; i++) {
    newArr.push(arr[i])
  }
  return newArr
}
const newData = take<number>([1, 2, 3, 4, 5], 2)
newData.forEach((item: string) => {})
console.log(newData)

即使在调用函数时,不写,ts也能自动识别到传入的数组的类型,从而约束之后使用这个参数时的类型。 在类型别名中使用泛型:

type Callback<T> = (n: T, i: number) => boolean
function filter<T>(arr: T[], callback: Callback<T>): T[] {
  const newArr: T[] = []
  arr.forEach((n, i) => {
    if (callback(n, i)) {
      newArr.push(n)
    }
  })
  return newArr
}
const data = [3, 4, 5, 6]
console.log(filter(data, n => n % 2 !== 0))

在接口中使用泛型:

interface Callback<T>{
  (n: T, i: number): boolean
}
function filter<T>(arr: T[], callback: Callback<T>): T[] {
  const newArr: T[] = []
  arr.forEach((n, i) => {
    if (callback(n, i)) {
      newArr.push(n)
    }
  })
  return newArr
}
const data = [3, 4, 5, 6]
console.log(filter(data, n => n % 2 !== 0))

在类中使用泛型:

class User<T> {
 name: string;
 private _age: T;
 sex: "男" | "女";

 constructor(name: string, sex: "男" | "女" = "男") {
  this.name = name;
  this.sex = sex;
 }
 setAge(value: T) {
  this._age = value
 }
 getAge() {
  return this._age
 }
}
const user = new User("zhangsan");
user.setAge(28)
console.log(user.getAge())

泛型约束

interface hasName {
  name: string
}
function nameToUppercase<T extends hasName>(obj: T): T {
  obj.name = obj.name.split(' ').map(s => s[0].toUpperCase() + s.substr(1)).join(' ')
  return obj
}
const data = {
  name: 'zhang san',
  age: 28,
  sex: '男'
}
const newData = nameToUppercase(data)
console.log(newData.name)

一个泛型要求必须包含某些类型

多泛型

// 将两个数组进行混合 [1, 2, 3]['a', 'b', 'c']进行混合 得到[1, 'a', 2, 'b', 3, 'c']
function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
  if (arr1.length !== arr2.length) {
    throw new Error('两个数组长度不相等')
  }
  let res: (T | K)[] = []
  for (let i = 0; i < arr1.length; i++) {
    res.push(arr1[i])
    res.push(arr2[i])
  }
  return res
}

ts模块化

模块化相关配置

配置名称含义
module设置编译结果中使用的模块化标准
moduleResolution设置解析模块的模式
noImplicitUseStrict编译结果中不包含“use strict”
removeComments编译结果移除注释
noEmitOnError错误时不生成编译结果
esModuleInterop启用es模块化交互非es模块导出
ts中如何书写模块化语句
ts在es6的模块化标准出现之前,有自己的单独的一套模块化语句,在es6出现之后,统一使用es6的模块化标准。
新建一个文件module.ts

超级详细的typescript入坑指南

index.ts中引入(注意:引入时不能加文件后缀,即’./module.ts’,因为编译后不会存在ts文件,无法找到这个文件):

超级详细的typescript入坑指南

即使不写import { num, sum } from './module',在使用的地方也可以快速引入

超级详细的typescript入坑指南

点击快速修复

超级详细的typescript入坑指南

可以直接在有这个声明的文件快速导入

但是这种快速导入只作用于一般的导出,例如上述例子中,直接导出一个声明,export default是无法快速导入的

超级详细的typescript入坑指南

因为默认导出是没有名称的,在引入时可以随意修改名称,所以ts无法识别

超级详细的typescript入坑指南

转载自:https://juejin.cn/post/7216252920768921660
评论
请登录