TypeScript 整理

320 阅读20分钟

TypeScript 是什么

简单来说TS是一种由微软开发的编程语言,其为JS的一个超集,且本质上向这个语言添加了可选的静态类型与基于类的面向对象编程,规范化语言的同时对JS进行了补强

TS与JS的区别

层面TypeScriptJavaScript
语言层面TS为JS的超集,用于解决大型项目的代码复杂性一种脚本类语言
类型层面强类型,支持静态与动态类型弱类型,没有静态类型
执行层面最终会被编译成JS代码才能执行,编译期间发现并纠正错误直接在浏览器中使用
支持层面支持模块、泛型以及接口,代码更易维护不支持模块、泛型以及接口

优点

  • 增强了代码的可读性与可维护性,编程者可通过定制好的接口快速判断当前数据内容
  • 增强了代码的健壮性,TS为强制类型语言,在遇到类型错误时会及时报错并注解错误内容,节约试错成本
  • 对JS有良好的包容性,即使在TS文件中不使用TS语法也不会有问题
  • 对JS的类型进行了进一步的补强,使开发更为安全,项目数据逻辑更为清晰

缺点

  • 有一定的学习成本,需要理解接口、泛型、类、枚举类型等概念
  • 由于需要多写一些类型的定义,短期会增加部分开发成本
  • 与部分库或是技术结合的不是很完美(Vue3之前对TS支持不完善)

安装与使用 TypeScript

安装TS

npm i -g typescript  // 全局安装 TS
tsc -v               // 查看 TS 版本

注:推荐用vscode搭配相关TS插件使用

使用TS

项目初始化

新建文件夹并打开控制台输入 tsc --init 生成 tsconfig.json 文件

TS执行相关

  • 控制台使用 tsc 文件名 可生成对应 JS 文件
  • 控制台直接输入 tsc 则会默认执行 tsconfig 文件中的内容
  • 若是需要生成特定TS文件的JS代码则需在 tsconfig 文件首部 "include":["文件名1.ts" , "文件名2.ts"], 
  • 不想让指定文件编译可在文件首部 "exclude":["文件1.ts" , "文件2.ts"],
  • tsconfig中的 "files" 与include简单使用是一致的,但是files不会被exclude排除,include可以写正则且会被exclude排除

tsconfig.json重要条目(逐步更新)

{
  //修改后需重启项目
  "include": ["tsconfig文件.ts"],             // 列表中文件会编译为js
  "exclude": [],                              // 列表中文件不编译为js
  "compilerOptions": {
    "sourceMap": true,                        // 开启后编译时生成 sourceMap
    "outDir": "./build",                      // 生成js所存放的目录
    "rootDir": "./",                          // 入口文件,编译器会在该目录下寻找ts文件
    "removeComments": true,                   // 编译去除注
    "strict": true,                           // 编译与书写规范要严格按照ts规范
    "noImplicitAny": true,                    // 是否允许你的注解类型any不用特意标明,改为false时不强制要求
    "strictNullChecks": true,                 // 是否允许ts文件中有 null 出现,false 为允许
    "noUnusedLocals": true,                   // 对未使用的变量进行提示
    "noUnusedParameters": true,               // 对未使用的方法进行提示
  }
}

TypeScript 基础类型

Boolean 类型

该类型为简单的 true / false 值

let isDone: boolean = false;  // 定义一个初始为 false 的 boolean 变量

Number 类型

该类型为默认为浮点数的数字,其支持二进制、八进制、十进制与十六进制

let num: number = 6;  // 定义一个初始为6的浮点变量

String 类型

可使用( "" )/( '' )/( `` )定义一条字符串,并且可以使用 ${ expr } 嵌入表达式

 let str: string = `name:${userName}`;    // 定义一条字符串,其值为 name:+ 变量值

Array 类型

let list: number[] = {1, 2, 3};        // 定义一个使用 number 类型元素组成的数组
let list: Array<number> = {1, 2, 3};   // 使用数组泛型定义一个数组

Enum 类型

该类型是对JS标准数据类型的一个补充,使用枚举类型可为一组数值赋予友好的名字

数字枚举

这种情况默认会从0开始为元素进行递推编号,也可以手动执行成员数值

enum Color {Red = 1, Green, Blue};     // 定义一个数字枚举类型的 Color ,其编号从1开始递推
let colorName: string = Color[2];      // 此处 Color 值为 Green

字符串枚举

在 TS2.4 版本以上允许使用字符串枚举,在单个字符串枚举中每个成员都需使用字符串字面量或另一个字符串枚举成员进行初始化

enum Color {Red = "RED", Green = "GREEN", Blue = "BLUE"};  //定义一个字符串枚举类型的 Color
let colorName: string = Color["Green"];   //此处 Color 值为 GREE

Any 类型

该类型可代表任意类型,其为类型系统的顶级类型(全局超集类型)

let test: any = "wuhu";   
test = 111;               // ok 
test = true;              // ok

Unknown 类型

类似于 any ,所有类型都可以赋值给该类型,但是该类型不能赋值给已有非 any 或 unknown 类型的变量

let value: unknown;
let value1: any = value;     // ok
let value2: unknown = value; // ok 
let value3: number = value;  // Error
let value4: string = value;  // Error

Tuple 类型

该类型表示一个已知元素数量与类型的数组,各元素的类型不必相同

let x: [string, number];     // 定义一个第一位为 string 类型,第二位为 number 类型的元组
x = ['hello', 10];           // 正确赋值
x = [10, 'hello'];           // 类型位没有一一对应,错误赋值

当访问一个已知索引的元素会得到其正确类型,若该类型上并没有调用方法则会报错

x[0].substr(1);              // ok
x[1].substr(1);              // Error, 'number' does not have 'substr'

当访问一个越界元素会使用联合类型替代(在此例中就是 string | number )

x[3] = "world";              // ok 字符串可以赋值给 string | number
x[6] = true;                 // Error 布尔不属于该联合类型

Void 类型

该类型表示没有任何类型,通常用于处理没有返回值的函数

function test(): void{};     // 声明一个没有返回值的函数

Null 与 Undefined 类型

这两种类型为所有类型的子级,非严格模式下可赋值给其他类型变量,严格模式则只能赋值给 void 或是他们各自类型

Never 类型

该类型用于表示永不存在的值,可用于指定根本不会有返回值的函数返回值

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

其可用于收窄的兜底检测类型

interface Foo {
  type: 'foo'
}


interface Bar {
  type: 'bar'
}


type All = Foo | Bar  // 若日后修改了此处的类型而没有增加类型 case 判断则会赋值给 never 类型变量导致报错
function handleValue(val: All) {
  switch (val.type) {
    case 'foo':
      // 这里 val 被收窄为 Foo
      break
    case 'bar':
      // val 在这里是 Bar
      break
    default:
      // val 在这里是 never
      const exhaustiveCheck: never = val  // 此处用于兜底验证,确保代码的健壮性
      break
  }
}

在 TS3.7 之后返回 never 的函数会被纳入控制流分析,其之后的代码会被判定为 unreachable ,可用于 unreachable code 分析

TypeScript 断言

但编程者比TS更了解某个值的详情信息时可使用类型断言强制为元素定义类型

尖括号

let str: any = "str"
let len: number = (<string>str).length;

as 语法

let str: any = "str"
let len: number = (str as string).length;

类型守卫

类型守护为可执行时用于确保类型在一定范围内的表达式

其主要思想为尝试检测属性、方法或原型以确定如何处理值

in 关键字

通过判断是否存在决定执行逻辑

interface type1{
  firstName: string;
}
interface type2{
  lastName: string;
}
type totalName = type1 | type2;     
function dosth(name: totalName){
  if("firstName" in name){ ... }      // 通过 in 判断是否存在该属性名
  if("lastName" in name){ ... }
}

typeof 关键字

通过类型判断决定执行逻辑

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") { ... }    // 通过 typeof 判断类型
  if (typeof padding === "string") { ... }
}

instanceof 关键字

通过原型链判断决定执行逻辑

interface test{
  dosth: string;
}
class testClass implements test{
  constructor(param: string){
    this.str: string = param;
  }
  dosth(): string{
    return this.str;
  }
}
let tester: test = new testClass('wuhu')
if(tester instanceof test){ ... }        // 检测test是否处于tester的原型链上

联合类型与类型别名

联合类型

联合类型可以理解为逻辑中的“ 或“ ,其通常与 null 或 undefined 一起使用

let name: string | undefined;          // 可接收 string 类型或是 undefined 类型,下同
const fun = (name: string | undefined) => { ... }

类型别名

类型别名用于给一个类型或多个类型联合起一个新名字

type Massage = string | string[];     // 将 string 与 string[] 联合命名为 Message
let greet = (message: Message) => { ... };  // 使用时按照正常类型使用即可

可辨识联合

该类型也被称为代数数据类型或是标签联合类型,其包含三个要点

  1. 可辨识:要求联合类型中的每一个成员都有一个相同的单例类型属性
  2. 联合类型:主类型中可包含多个子类型,各子类型间为 ”或“ 关系
  3. 类型守卫:用于区分各子类型并创建相应的逻辑空间
interface test1 {
  vType: "test1";                 // 两接口都拥有相同的可辨识属性 vtype
  ...
}
interface test2 {
  vType: "test2"; 
  ...
}


type totalType = test1 | test2;   // 此处创建联合类型 totalType


function func(arg: totalType){
  switch(arg.vtype){              // 此处使用类型守卫对逻辑区块进行区分
    case "test1": { ... };        // 执行相应区块逻辑
    case "test2": { ... };
  }
}
const myTest: test1 = { vType: "test1", ... };
func(myTest);

交叉类型

该类型可将多个类型合并为一个类型,其包含所需所有类型的特性(类似于逻辑中的 ”与“ )

interface test1{
  value1: string;
  value2: string;
}
interface test2{
  value3: string;
  value4: string;
}
const totalTest = test1 & test2  // 此时 totalTest 拥有 test1 与 test2 的所有特性

TypeScript 函数

TypeScript 函数与 JavaScript 函数区别

TypeScriptJavaScript
含有类型无类型
函数类型非函数类型
必填和可选参数所有参数可选
可函数重载无函数重载

TypeScript 函数详解

构建语法

普通函数写法

// 创建函数名为 test 的普通函数并传入 string 类型与 number 类型参数,函数最终返回 string 类型结果
let ans = function(arg1: string, arg2: number): string{ ... }

箭头函数写法

let ans = (arg1:string, arg2:number): string => { ... } // 构建函数并将结果赋予 ans

函数类型

定义方式

// 此处定义名为 template ,传入 string 与 number 类型最终返回 string 类型结果的函数类型
let template: (arg1: string, arg2: number): string;  

可选参数与默认参数

function test(arg1: string, arg2?: number):string { ... }  // 参数二即为可选参数
// 注:由于函数是按顺序获取参数,可选参数一定要置于必填项后,否则影响传参顺序
function test(arg1: string = "str", arg2: number):string { ... }

剩余参数

function test(arg1:string, ...args): string{ ... } // args 即为剩余参数数组

函数重载

  • 函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的能力
  • 实现方法是为同一函数提供多个函数类型定义,编译器会根据这个列表处理函数的调用
// 函数重载
function add(a: number, b: number): number;
function add(a: string, b: string): string;


// 方法重载,此时要求同一类中方法名相同当时参数列表不同
class test{
  add(a: number, b: number): number;
  add(a: string, b: string): string;
}
//TS在处理函数重载时会查找重载列表,依次尝试定义,故一定要把最精确的定义放在最前面

可编译语言函数重载与TS函数重载的区别

  • 可编译语言函数重载发生在编译时,编译器可清楚的将每一处同名函数调用至开发者写的不同函数实现,同名是开发者看到的假象,实质上真正编译后程序中存在两个不同命函数分别对应重载位置
  • TS秉承JS的动态类型没有编译阶段,其实现的函数重载只是允许给这样的函数标注多个类型,在真正只有一个函数实现,处理逻辑时还需要在该函数实现中通过类型判断来区分不同逻辑

TypeScript 接口

接口定义

  • 在面向对象语言中接口是非常重要的,其是对行为的抽象,具体如何行动需要由类去实现
  • 除了用于对类的一部分行为进行抽象外,也常用于对对象的形状进行描述

接口详解

对象的形状

interface Person {
  name: string;
  age: number;
}

let Semlinker: Person = {
  name: "Semlinker",
  age: 33,
};

可选、只读属性

interface Person {
  readonly name: string;     // 设置为 readonly 的属性为只读属性
  age?: number;              // 名称后加 ? 的属性为可选属性实不实现都不报错
}

除此之外,TS还提供了 ReadonlyArray 类型,其设置数组中所有值为只读,确保数组创建后不能被改变

接口继承

接口间可使用 extends 关键字继承接口或类

interface A{
  value1: number;
  func(): void;
}
interface B extends A{       // 此时接口 B 继承了 A 中所有属性与方法 
  value2: string;
  func2(): string;
}

接口的实现

接口的实现使用 implements 关键字

interface A {
  value: string;
  func(): void;
}
interface C {
  value2: number;
  func2(): number;
}
class B implements A, C{        // 在 B 类中实现 A 与 C 接口(在一个类中可以实现多个接口)
  constructor(){ ... }
  value: string;
  value2: number;
  func: ()=>{ ... };
  func2: ()=>{ ... };
}

辨析 extends 与 implements

基础定义

  • extends 为继承一个新的接口或是类,从父类或是接口继承所有的属性和方法,不可重写属性,但是可以重写方法
  • implements 为实现一个新的类,从父类或是接口实现所有的属性与方法,可以重写属性和方法

使用注意

  • 接口不能实现接口或是类,只有类可以实现接口或类
  • 接口可以继承接口或类
  • 类不能继承接口,类只能继承类

TypeScript 类

TS中的类本质上仍然是JS构造函数的语法糖,TS在JS类的基础上进行了补强

TS类

基础写法

class Greeter{
  greeing: string;                     // 创建属性
  constructor(message: string){        // 初始化构造函数
    this.greeting = message;           // 引用任意一个类成员时需要使用 this
  }
  greet(){ ... }                       // 创建实例方法
}
let greeter = new Creeter("wuhu")      // 使用 new 构造 Greeter 类的一个实例

类的继承

什么是继承

基于类的程序设计中允许使用继承来扩展现有的类,扩展后的子类同时拥有父类与自身原本的属性与方法,继承是一种 is-a 关系

在面向对象程序的领域里,is-a 指的是在抽象(比如类或类型)之间体现的包容关系。

例如类D是另一个类B的子类(类B是类D的父类),则D被包容在B内。换句话说,通常"D is a B"指的是,概念体D物是概念体B物的特殊化,而概念体B物是概念体D物的广泛化1。

举例来说,水果是苹果、橘子、芒果与其他水果的广泛化。

类与类继承方式

类与类间可使用 extends 关键字进行继承

class Animal{
  name: string;
  constructor(theName: string){ this.name = theName; }
  move(){ ... }
}
class Snake extends Animal{                   // Snake 中同时包含父类与自身的方法与属性
  constructor(name: string){ super(name); }   // 此处必须使用 super 对父类进行传值
  alerm(){ ... }
}

类中的修饰符

public 公有修饰符

该修饰符为默认修饰符,无论内外都可以进行访问,需要注意的是在加入修饰符后类的写法可以简写为

class A {
  constructor(public name: string){}     // 此处的 name 属性相当于直接在 class 内部定义
}

private 私有修饰符

当成员被标记为 private 时,其就不能在被声明类的外部访问

class A{
  constructor(private name: string){}   // 定义一个私有变量 name 并为其赋传入的值
}
new A("wuhu").name;                     // Error: 'name'是私有的
  • TypeScript 使用的是结构性类型系统,一般情况下比较两种不同类时只观察其对应位置类型是否兼容,如果所有成员的类型兼容则认定两个类的类型兼容
  • 当比较含有 private 或 protected 成员的类时,只有两个类中同时存在特殊标识符元素且都来自同一处声明时才会认定两个类是兼容的
class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {            // 继承后的 name 来自同一声明
    constructor() { super("Rhino"); }
}
class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
animal = rhino;                         // OK 此处特殊修饰符元素来自同一个声明
animal = employee;                      // Error: Animal 与 Employee 不兼容.

protected 保护修饰符

当成员被标记为 protected 时该成员尽可在声明类内部或是派生类中访问,其余地方不可访问

class A{
  constructor(protected name: string){}
}
class B extends A{
  constructor(name: string){ super(name) }
  public func(){
    return this.name
  }
}
let test = new B("wuhu");
test.func();                             // ok 'wuhu'
test.name;                               // Error 不可在外部访问 name 属性

protected 修饰符也可用于构造函数,其意味着该类不能再包含它的类外被实例化,但是可以被继承

class A{
  protected constructor(public name: string){}
}
class B extends A{
  constructor(name: string){ super(name) }
  public func(){
    return this.name
  }
}
let test1 = new B('wuhu');                // OK
let test2 = new A('wuhu');                // Error: A 的构造函数是被保护的

readonly 修饰符

该修饰符可将属性设置为只读,只读属性必须在声明时或构造函数中初始化

class A{
  constructor(readonly name: string){}
}
let test = new A('wuhu');
test.name = "azhe";                        // Error: name 是只读的

存取器

TS中支持通过 getter/setter 截取对对象成员的访问

class A{
  private _name: string;
  get name(): string{                      // 外部获取时调用 get
    return this._name
  }
  set name(newName: string):string{        // 外部改变时调用 set
    this._name = newName + newName;
  }
}

需要注意的是只带有 get 不带有 set 的存取器自动被推断为 readonly

静态属性

使用 static 标识符定义的属性存在于类的本身而不是类的实例上

class A{
  static values = { X:0, y:0 };            // 此处在类的 prototype 上定义静态属性 values
  constructor(public name: string){}       // 此处在类本身上定义实例属性 name
  func(){ return A.values.x }              // 取用时需要加上类名
}

抽象类

抽象类做为其他派生类的基类使用,相对于接口,抽象类中可以包含成员的实现细节,抽象类中可通过 abstract 关键字定义抽象方法

abstract class A{                          // 定义抽象类 A
  abstract sayName(): void;                // 定义抽象方法 sayName,sayName 必须在派生类中被实现
  func(): string{                          // 定义具有细节的方法 func
    return "wuhu";
  }
  constructor(public name: string){}
}
class B extends A{
  constructor(name: string){ super(name); }
  sayName(): void{
    console.log(this.name);
  }
  saywuhu(): void{
    console.log("wuhu")
  }
} 
let test = new A("aaa");                   // Error: 不能创建抽象类的实例
let test2 = new B("bbb");                  // OK
test2.func();                              // OK
test2.sayName();                           // OK
test2.sayWuhu();                           // OK
let test3: A;                              // 创建抽象类的引用
test3 = new B('bbb').sayWuhu();            // Error: 方法在声明的抽象类中不存在

注:抽象类中的抽象方法不包含具体实现且必须在派生类中被实现,其与接口类似都是定义方法签名但是不包含方法体

TypeScript 泛型

泛型简介

  • 泛型使创建的组件拥有良好的可重用性,且可以灵活的支持当前以及未来的数据类型
  • 使用泛型创建的可重用组件可以支持多种类型的数据,用户可根据自身数据需求使用组件
  • 泛型是一种允许同一个函数接受不同类型参数的模板,相对于any类型泛型会保留参数类型且更加规范

泛型语法

单泛型

image.png

图中 "T" 即为泛型,其会被替换为用户传入的特定类型,例如用户调用 identity(1) 则会将 Number 类型作为泛型的填充值,它会被填充到当前任何出现泛型 "T" 的位置( T 可以使用任意有效名称替代 )

多泛型

image.png

根据图示,泛型 "T" 与泛型 "U" 会被按顺序替换为 Number 类型与 String 类型并填充至后续泛型位置

泛型接口

泛型还可通过接口传入规定接口中的数据类型

interface A <T>{        // 传入泛型 "T"
  (arg: T): T;          // 参数 arg 类型将会被标识为泛型传入的值,此处意为规定一个传入参数与返回值为 T 类型的函数
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: A <number> = identity;   // 规定函数的对应接口并给泛型赋值

泛型类

泛型也可以规定类中数据类型

class A <T> {                            // 此处规定泛型名称用于传入泛型
  zeroValue: T;
  add: (x: T, y: T) => T;                         
}


let B = new A <number>();                // 将 Number 填充至泛型 "T" 中
B.add = function (x, y) {
  return x + y;
};

泛型约束

可通过 extends 关键字添加泛型约束

interface A{                            // 泛型约束条件
  length: number;
}
function B<T extends A>(arg: T): T {    // 需要保证传入的参数带有 length 属性
  return arg;
}

泛型相关操作符以及工具类型

为更好的解释工具类型,首先从几个相关操作符讲起

typeof

获取变量声明或是对象类型

// 获取变量声明
interface Person { ... }
const sem: Person = { ... };
type Sem= typeof sem;                   // -> Person
// 获取对象类型
function toArray(x: number): Array<number> { ... }
type Func = typeof toArray;             // -> (x: number) => number[]

keyof

用于获取某种类型的所有键,返回联合类型

interface Person {
  name: string;
  age: number;
}
type K1 = keyof Person;                 // "name" | "age"
type K2 = keyof Person[];               // "length" | "toString" | "pop" | "push" | "concat" | "join" 
// 使用索引签名,这个索引签名表示了当使用 string 索引时会得到 Person 中类型的返回值
type K3 = keyof { [index: string]: Person };// string | number 

in

用于遍历枚举类型

type Keys = "a" | "b" | "c"        
type Obj =  {
  [p in Keys]: any                      // 遍历 keys 并将类型设置为 any
} // -> { a: any, b: any, c: any }

介绍完几个基础操作符,后面正式开始说明常见泛型工具类型

Partial

其作用是将某个类型中的属性全部变为可选项

// 具体定义
type Partial<T> = {
  [P in keyof T]?: T[P];                 // 遍历处理逻辑
};

首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历并将值赋予 P ,最后通过 T[P] 取得相应的属性值,其中间的 ? 用于将所有属性变为可选

Required

其作用是将传入的属性变为必选项

type Required<T> = { 
  [P in keyof T]-?: T[P];               // 与 Partial相反,此处为 -?
};

该操作符与 Partial 相反,使用 -? 将每项中的 ? 删除变为必选项

Readonly

其作用是将传入的属性变为只读选项

type Readonly<T> = { 
  readonly [P in keyof T]: T[P];         // 遍历设置 readonly 
};

Record

将 K 中的属性值转化为 T 类型

type Record<K extends keyof any, T> = {  // 传入目标 K 与特定类型 T
  [P in K]: T;                           // 使用 in 操作符遍历目标 K 各项并设置类型为 T
};

具体使用

type petsGroup = 'dog' | 'cat' | 'fish';
interface IPetInfo {
    name:string,
    age:number,
}
type IPets = Record<petsGroup, IPetInfo>;

Pick

从 T 中取出 K 的属性

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

具体使用

interface A{
	name: string;
	age: number;
	like: string[];
}
interface B extends Pick< A, "name" | "age" > {}; // 提取 A 接口中的 name 与 age 属性赋予 B 接口

ReturnType

其用于获取函数的返回值类型

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R      // 此处的 infer 用于声明一个变量来承载传入函数签名的返回值类型
  ? R
  : any;
// 总体逻辑为判断传入泛型是否为函数,如果是则返回其返回值类型 "R"

该写法还可用于提取参数类型(此处不是工具类型具体用法)

type A<T> = T extends (
  params: infer R 
) => any ? R : T
// 总体逻辑为判断传入类型是否为函数,若是则返回其参数类型 "R"

to be continue~