关于TypeScript

简称TS,TypeScript是JS的超集,是一个可选的、静态的类型系统。

1. 类型系统: 对代码中所有的标识符(变量、函数、参数、返回值)进行类型检查.

2. 静态的:无论是浏览器环境,还是node环境,无法直接识别ts代码,类型检查发生在编译的时候,而非运行时.

TS的常识

JS原罪

  • js语言本身的特性,决定了该语言无法适应大型的复杂的项目
  • 弱类型:某个变量,可以随时更换类型。
  • 解释性:错误发生的时间,是在运行时

JS开发中的问题

  • 使用了不存在的变量、函数或成员
  • 把一个不确定的类型当作一个确定的类型处理
  • 在使用null或undefined的成员

开发环境配置

安装

npm install typescript -g 

默认情况下,TS会做出下面几种假设:

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

有两种方式更改以上假设:

  1. 使用tsc命令行的时候,加上选项参数
  2. 使用ts配置文件,更改编译选项

TS配置文件

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "lib": ["ES2022"],  // 执行环境
    "outDir": "./dist", // 编译输出目录
    "strictNullChecks": true // 严格空类型检查
  },
  // "files": []          //要编译的文件
  "include": ["./src"] //要编译的目标目录
}

更多配置详见tsconfig.json 是什么,使用了配置文件后,使用tsc进行编译时,不能跟上文件名,如果跟上文件名,会忽略配置文件。

使用第三方库

@types/node

@types是一个ts官方的类型库,其中包含了很多对js代码的类型描述。

npm i -D @types/node  

ts-node

将ts代码在内存中完成编译,同时完成运行。

npm i ts-node -g   
# ts-node src/index.ts  #编译index.ts文件

nodemon

用于检测文件的变化,结合ts-node实现文件改变自动编译并执行

npm i nodemon -g  
nodemon --exec ts-node src/index.ts  # 使用

package.json中配置:

{
  "scripts": {
    "dev": "nodemon --watch src -e ts --exec ts-node src/index.ts"
  },
  "devDependencies": {
    "@types/node": "^18.15.6"
  }
}

参数详解:

  • --watch : 监听src目录
  • -e : 指定监听的文件类型
  • -- exec: 执行命令

数据类型

基本类型

  • number:数字
  • string:字符串
  • boolean:布尔
  • 数组
  • object: 对象
  • null 和 undefined

基本类型约束,仅需要在 变量、函数的参数、函数的返回值位置加上:类型,ts在很多场景中可以完成类型推导。

any: 表示任意类型,对该类型,ts不进行类型检查

null和undefined是所有其他类型的子类型,它们可以赋值给其他类型

通过添加strictNullChecks:true,可以获得更严格的空类型检查,null和undefined只能赋值给自身。

其他常用类型

  • 联合类型:多种类型任选其一

    • 配合类型保护进行判断
      类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,typeof可以触发类型保护。
  • void类型:通常用于约束函数的返回值,表示该函数没有任何返回
  • never类型:通常用于约束函数的返回值,表示该函数永远不可能结束
  • 字面量类型:使用一个值进行约束
  • 元祖类型(Tuple): 一个固定长度的数组,并且数组中每一项的类型确定
  • any类型: any类型可以绕过类型检查,因此,any类型的数据可以赋值给任意类型

函数的相关约束

/**
 * 得到a*b的结果
 * @param a 
 * @param b 
 */
function combine(a:number, b:number):number;
/**
 * 得到a和b拼接的结果
 * @param a 
 * @param b 
 */
function combine(a:string, b:string):string;
function combine(a: number | string, b: number | string): number | string {
    if (typeof a === "number" && typeof b === "number") {
        return a * b;
    }
    else if (typeof a === "string" && typeof b === "string") {
        return a + b;
    }
    throw new Error("a和b必须是相同的类型");
}

const result = combine("a","b")

函数重载:在函数实现之前,对函数调用的多种情况进行声明

可选参数:可以在某些参数名后加上问号,表示该参数可以不用传递。可选参数必须在参数列表的末尾。

扩展类型

扩展类型:类型别名、枚举、接口、类

类型别名

对已知的一些类型定义名称.

// type 类型名 = ...

type Gender = "男" | "女"
type User = {
    name:string
    age:number
    gender:Gender
}

let u:User

u = {
    name:"sdfd",
    gender:"男",
    age:34
}

function getUsers(g:Gender):User[] {
    return [];
}

枚举

枚举通常用于约束某个变量的取值范围。字面量和联合类型配合使用,也可以达到同样的目标。

字面量类型的问题

  • 在类型约束位置,会产生重复代码。可以使用类型别名解决该问题。
  • 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。
  • 字面量类型不会进入到编译结果。

定义

enum 枚举名 {
  枚举字段1 = 值1,
  枚举字段2 = 值2,
  // ...
}

例子

enum Gender {
  male = "男",
  female = "女",
}

let gender: Gender

gender = Gender.male;

枚举会出现在编译结果中,编译结果中表现为对象。

枚举的规则:

  • 枚举的字段值可以是字符串或数字
  • 数字枚举的值会自动==自增==
  • 被数字枚举约束的变量,可以直接赋值为数字
  • 数字枚举的编译结果 和 字符串枚举有差异
  • 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
  • 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值

位运算

enum Permission {
    Read = 1,   // 0001
    Write = 2,  // 0010
    Create = 4, // 0100
    Delete = 8  // 1000
}

//1. 如何组合权限
//使用或运算
//0001
//或
//0010
//0011
let p: Permission = Permission.Read | Permission.Write;

//2. 如何判断是否拥有某个权限
//0011
//且
//0010
//0010
function hasPermission(target: Permission, per: Permission) {
    return (target & per) === per;
}
//判断变量p是否拥有可读权限

//3. 如何删除某个权限
//0011
//异或
//0010
//0001
p = p ^ Permission.Write;
console.log(hasPermission(p, Permission.Write));

接口

接口:inteface

TypeScript的接口:用于约束类、对象、函数的契约(标准),和类型别名一样,接口,不出现在编译结果中。

契约(标准)的形式:

  • API文档,弱标准
  • 代码约束,强标准

接口约束对象

interface User {
  name: string
  age: number
}

let u: User = {
  name: "张三",
  age: 18
}

接口约束函数

  1. 函数作为对象成员
interface User {
  name: string
  age: number
  sayHello(): void
}

let u: User = {
  name: "张三",
  age: 18,
  sayHello() {
    console.log("asfadasfaf");
  }
}
  1. 约束函数
type Condition = (n: number) => boolean

interface Condition {
  (n: number): boolean
}

function sum(numbers: number[], callBack: Condition) {
  let s = 0;
  numbers.forEach(n => {
    if (callBack(n)) {
      s += n;
    }
  })
  return s;
}

接口继承

接口可以继承,可以通过接口之间的继承,实现多种接口的组合,使用类型别名可以实现类似的组合效果,需要通过&,它叫做交叉类型

它们的区别:

  • 子接口不能覆盖父接口的成员
  • 交叉类型会把相同成员的类型进行交叉
interface A {
  T1: string
}

interface B {
  T2: number
}

interface C extends A, B {
  T3: boolean
}

// 使用类型别名实现以上效果
type C = {
    T1: number
    T3: boolean
} & A & B

readonly

只读修饰符,修饰的目标是只读,只读修饰符不在编译结果中。

type User = {
  readonly id: string
  name: string
  age: number,
  readonly arr: readonly string[]
}

interface User = {
  readonly id: string
  name: string
  age: number,
  readonly arr: readonly string[]
}

类型兼容性

B->A,如果能完成赋值,则B和A类型兼容

鸭子辨型法(子结构辨型法):目标类型需要某一些特征,赋值的类型只要能满足该特征即可

  • 基本类型:完全匹配
  • 对象类型:鸭子辨型法

类型断言,当直接使用对象字面量赋值的时候,会进行更加严格的判断

  • 函数类型
interface Duck {
  sound: "嘎嘎嘎"
  swin(): void
}

let person = {
  name: "伪装成鸭子的人",
  age: 11,
  sound: "嘎嘎嘎" as "嘎嘎嘎",
  swin() {
    console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
  }
}

let duck: Duck = {
  sound: "嘎嘎嘎" as "嘎嘎嘎",
  swin() {
    console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
  }
};

一切无比自然

参数:传递给目标函数的参数可以少,但不可以多

返回值:要求返回必须返回;不要求返回,你随意;

class User {
  readonly id: number; //不能改变
  gender: "男" | "女" = "男";
  pid?: string;
  private _publishNumber: number = 3; //每天一共可以发布多少篇文章
  private _curNumber: number = 0; //当前可以发布的文章数量

  constructor(public name: string, private _age: number) {
    this.id = Math.random();
  }

  set age(value: number) {
    if (value < 0) {
      this._age = 0;
    } else if (value > 200) {
      this._age = 200;
    } else {
      this._age = value;
    }
  }

  get age() {
    return Math.floor(this._age);
  }

  publish(title: string) {
    if (this._curNumber < this._publishNumber) {
      console.log("发布一篇文章:" + title);
      this._curNumber++;
    } else {
      console.log("你今日发布的文章数量已达到上限");
    }
  }
}

const u = new User("aa", 22);
//c#
u.age = 1.5;
console.log(u.age);

u.publish("文章1");
u.publish("文章2");
u.publish("文章3");
u.publish("文章4");
u.publish("文章5");
u.publish("文章6");

属性

使用属性列表来描述类中的属性

属性的初始化检查 strictPropertyInitialization:true

属性的初始化位置:

  1. 构造函数中
  2. 属性默认值

属性可以修饰为可选的

属性可以修饰为只读的

使用访问修饰符

访问修饰符可以控制类中的某个成员的访问权限

  • public:默认的访问修饰符,公开的,所有的代码均可访问
  • private:私有的,只有在类中可以访问
  • protected:...

Symble

属性简写

如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写

访问器

作用:用于控制属性的读取和赋值

泛型

有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)

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

const result = mixinArray([1, 3, 4], ["a", "b", "c"]);

result.forEach(r => console.log(r));

泛型:是指附属于函数、类、接口、类型别名之上的类型

泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有到调用时,才能确定它的类型

很多时候,TS会智能的根据传递的参数,推导出泛型的具体类型,如果无法完成推导,并且又没有传递具体的类型,默认为空对象

泛型可以设置默认值

在函数中使用泛型

在函数名之后写上<泛型名称>

在类型别名、接口、类中使用泛型

直接在名称后写上<泛型名称>

泛型约束

泛型约束,用于现实泛型的取值

模块化

配置名称含义
module设置编译结果中使用的模块化标准
moduleResolution设置解析模块的模式
noImplicitUseStrict编译结果中不包含"use strict"
removeComments编译结果移除注释
noEmitOnError错误时不生成编译结果
esModuleInterop启用es模块化交互非es模块导出
前端领域中的模块化标准:ES6、commonjs、amd、umd、system、esnext

模块化语句

TS中,导入和导出模块,统一使用ES6的模块化标准

编译结果中的模块化

TS中的模块化在编译结果中:

  • 如果编译结果的模块化标准是ES6: 没有区别
  • 如果编译结果的模块化标准是commonjs:导出的声明会变成exports的属性,默认的导出会变成exports的default属性;

在TS中书写commonjs模块化代码

导出:export = xxx

导入:import xxx = require("xxx")

模块解析

模块解析:应该从什么位置寻找模块

TS中,有两种模块解析策略

  • classic:经典
  • node:node解析策略(唯一的变化,是将js替换为ts)

    • 相对路径require("./xxx")
    • 非相对模块require("xxx")