《TypeScript 全面进阶指南》学习心得

191 阅读12分钟

本篇是掘金小册《TypeScript 全面进阶指南》的学习心得分享。这也是自己写的第二篇文章顺便记录一下自己的成长哈哈哈。

Day1

开篇:选择 TypeScript 还是 Javascript 作为项目中主要的语言的一些思考

Javascript 这么灵活,我们为什么需要 TypeScript 来对我们进行约束? 使用 TypeScript 能给我们带来什么好处?

关于 TypeScript 的质疑

  • TypeScript 限制了 JavaScript 的灵活性;
  • TypeScript 并不能提高应用程序的性能;
  • TypeScript 开发需要更多额外的类型代码。

TS 和 JS 中的优劣在文章中已经尽数说明,我个人认为一项技术的出现必然是解决之前开发过程当中的问题,从而使我们能够在以后的开发当中去避免一些类似的问题,使用 TypeScript 还是 JavaScript 重要的是结合团队技术水平和编码风格以及项目去综合考虑选择适合当前项目的技术栈。

第二章:开发环境

插件

  • TypeScript Import 提供类型补全。
  • Move TS 方便重构时使用。
  • ErrorLens 将对应代码的 TS 报错信息展示在对应的位置。
  • Playground 临时 TS 编辑器

VS code 工作区配置

在 VS code 配置搜索处,搜索 'typescript Inlay Hints',找到 TS 扩展 可以根据自身喜好进行选择配置,可以展示函数参数类型、返回值等。

npm 包

  • ts-node 安装后可以直接在终端输入 ts-node index.ts 解析并执行ts文件, 在终端输出代码执行结果。
  • ts-node-dev 可以理解成能够监听文件改变而重新解析执行 TS 文件的这么一个东西,类似于 nodemon
  • tsd 可以避免繁琐的类型声明。

第三章:理解原始类型与对象类型

原始类型的类型标注

null 与 undefined

当未开启 strictNullChecks 检查的情况下,会被视作其他类型的子类型。比如 string 类型会被认为包含了 null 和 undefined

  • null 开发者手动对变量进行赋值,一般是即将对某个变量进行赋值,只是在赋值之前设置的默认值
  • undefined 一般是使用var声明的某个变量未赋值或对象当中的某个属性不存在时的默认值
void

void 表示空值,当函数没有返回值或者返回 void 0 时函数返回值类型会被推导为 void

当然 void 类型并不只能在函数返回当中使用,同时也可以将 void 类型的变量赋值为 nullundefined

const voidVar1: void = undefined;
const voidVar2: void = null; // 需要关闭 ts.config 配置中的 strictNullChecks

数组类型标注

TS 中的数组类型声明

const arr1: string[] = [];
const arr2: Array<string> = [];
// 以上两种声明方式完全等价

// 元组(tuple) 声明方式
const arr3: [number, string] = [1, 'hello'];

// 当我们不清楚后面两项需不需要赋值时可以选择 在类型后添加 ?,表示可选成员
const arr4: [number, string?, boolean?] = [1, 'hello'];
// const arr4: [number, string?, boolean?] = [1, , ,]; // 还可以这么玩
const arr5: [number, string | undefined, boolean | undefined] = [1, 'hello',]; // 与以上声明方式完全等价

具名元组: 可以在元组中的元素打上类似属性的标记,跟对象的属性名差不多,更偏向语义化。

const arr7: [name: string, age: number, male: boolean] = ['linbudu', 599, true];

// 使用 ?: 当作可选修饰符
const arr8: [name: string, age: number, male?: boolean] = ['linbudu', 599, true]; 

对象的类型标注

TS 中对象类型声明

const obj1: Object = {}
const obj2: object = {}
const obj3: {} = {}

除了以上几种简单粗暴的声明方式外,我们还可以给对象的类型赋值为 interfacetype 类型别名。

interface

可以理解为对象对外提供的接口结构,使用 ? 来标记属性为可选,属性同样会被视为 定义时的类型 | undefined

使用 interface 声明结构,并给对象进行类型标注:

interface IDescription {
  name: string;
  age: number;
  male?: boolean;
}

const obj1: IDescription = {
  name: 'linbudu',
  age: 599,
  male: true,
};

readOnly 修饰符: 将对象中的属性变成只读,不能进行修改。

interface 和 type

interface 用来描述对象、类的结构,而 type 用来将一个函数签名、一组联合类型、一个工具类型等等抽离成一个完整独立的类型

object、Object 以及 {}

原型链的顶端是 Object 以及 Function。这也意味着在 TS 中使用 Object 意味着所有原始类型和对象类型最终都会被指向Object,在 TypeScript 中就表现为Object包含所有类型,所以强烈不推荐使用Object及其他装箱类型(String、Number、Boolean)

// 对于 undefined、null、void 0 ,需要关闭 strictNullChecks 
const tmp1: Object = undefined; 
const tmp2: Object = null; 
const tmp3: Object = void 0;

object 只能声明所有非原始类型标注,只能声明对象类型标注如:对象、数组、函数这些。

const tmp1: object = undefined; // 不能将类型“undefined”分配给类型“object”。
const tmp2: object = () => {}
const tmp2: object = []

{} 表示空对象,它没有任何的属性也不能对它进行赋值,即便在声明时对其进行赋值也无法访问。

const obj: {} = { name: 'Jack' };
console.log(obj.name); // 类型“{}”上不存在属性“name”。
Symbol

Symbol 是 ES6 以后新增的一个数据类型,表示唯一值类型。在 TS 中也提供了对应的类型标注。

const uniqueSymbolFoo: unique symbol = Symbol("linbudu") 
// 类型不兼容 
const uniqueSymbolBar: unique symbol = uniqueSymbolFoo

Day2

字面量类型与联合类型

当属性值的类型可能是 "success" | "failed" 的时候可以使用具体的值来进行声明,这被称为字面量类型

联合类型 顾名思义指的是这个值既可以是 a类型 也可以是 b类型,它代表了一组类型的可用集合

interface ResponseData{
    code: number,
    status: "success" | "failed",
    data: null
}

对象字面量类型

对象字面量简而言之就是将对象具体的属性以及对应的值进行声明,实现时也意味着都需要一对一的进行实现,了解即可。

interface Userinfo {
   name: "Jack",
   age: 18
}

const userInfo: Userinfo = {
   name: "Jack",
   age: 18
}

枚举

枚举简单来说就是一组可以被更好管理和组织的常量,下面列举作用和用法:

  1. 普通枚举类型声明,枚举中存在双向映射,所以我们既可以通过 key 去访问 value 也可以通过 value 访问 key。
enum NavButton {
    HOME,
    CLASSIFY,
    MENU,
    PROFILE
}

const a = NavButton.HOME
console.log(a) // HOME
const b = NavButton[0] // 也可以通过下标进行访问
console.log(b) // HOME
  1. 同时我们也可以
enum NavButton {
    HOME, 
    CLASSIFY,
    MENU,
    PROFILE = "PROFILE",
}

const button: NavButton = NavButton.PROFILE 
console.log(button) // PROFILE

常量枚举

常量枚举在使用时并不能通过值来进行访问,而且如果并没有对成员进行赋值的话,默认值是从0开始依次递增。

const enum NavButton {
    HOME,
    CLASSIFY,
    MENU,
    PROFILE
}

const a = NavButton.HOME
console.log(a) // 0

const b = NavButton[0] // 报错: 只有使用字符串文本才能访问常数枚举成员。

Day3

函数

函数类型签名

函数类型标注的声明方式,对函数形参以及函数返回值的标注:

const fun = (name: string): number => {
	return name.length
}

下面将进行函数类型声明后实现具体函数的形式进行函数类型标注,但代码可读性较差,不推荐使用,如果需要则使用类型别名的形式进行声明:

// 方式一
const fun: (name: string) => number = function (name) {
	return name.length
}

// 方式二
type FunFoo = (name: string) => number 
const fun2:FunFoo = function (name) {
	return name.length
}

函数类型标注同样也可以使用 interface 进行声明:

interface FuncFooStruct { 
    (name: string): number 
}

void 类型

当函数没有返回值时即为 void 表示空、没有任何意义类型的值。同时需要注意的是当函数返回值为 return; 时函数返回值默认还是 void, 但其实用 undefined 更合适。因为函数内其实做了 return 操作

// const foo: () => void
const foo = () => { 
	return;
}
 

函数可选参数与rest参数

基本用法:

// 可选参数
const fun = (firstName: string, lastName?: string ): string => {
	return `${firstName}-${lastName}`;
};

const fun2 = (firstName: string, lastName: string = ""): string => {
	return `${firstName}-${lastName}`;
};

 // rest 参数 
const foo = (...arg: string[]): string => arg.reduce((p, v) => p + '-' + v, '');

const foo2 = (...arg:[string, string]): string => arg.reduce((p, v) => p + '-' + v, '');

可选参数需要在必选参数后,当然也可以给可选参数设置默认值,这样在设置了默认值的参数后继续增加可选参数。

函数重载

简单的来说函数重载指的是同一个函数名具有多种不同实现,比如函数入参时的参数类型、参数个数及其返回值类型存在多种。使用函数重载后可以将具体的入参与返回值类型进行关联。

函数重载包括:

  • 重载签名
  • 实现签名
function func(foo: number, bar: true): string;
function func(foo: number, bar?: false): number;
function func(foo: number, bar?: boolean): string | number {
  if (bar) {
    return String(foo);
  } else {
    return foo * 599;
  }
}

const res1 = func(599); // number
const res2 = func(599, true); // string
const res3 = func(599, false); // number

TS 中的重载与其他语言的重载不同(侧重签名的重载而非实现的重载)。

Class

类与类成员的类型签名

Class 类型相关的有:

  • 构造函数
  • 属性
  • 方法
  • 访问符

修饰符

  • public: 访问性修饰符,此类成员在类、类的实例、子类中都能被访问。
  • private: 访问性修饰符,此类成员仅能在类的内部被访问。
  • protected: 访问性修饰符,此类成员仅能在类与子类中被访问,你可以将类和类的实例当成两种概念,即一旦实例化完毕(出厂零件),那就和类(工厂)没关系了,即不允许再访问受保护的成员
  • readonly: 操作性修饰,此类成员只能被访问,不允许进行修改
class Foo {
  private prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  protected print(addon: string): void {
    console.log(`${this.prop} and ${addon}`)
  }

  public get propA(): string {
    return `${this.prop}+A`;
  }

  public set propA(value: string) {
    this.propA = `${value}+A`
  }
}

小技巧:在构造函数中对参数应用访问性修饰符,则不需要在外部进行类型标注

class Foo {
  constructor(public arg1: string, private arg2: boolean) { }
}

new Foo("linbudu", true)

关键字

static 关键字,可以标识一个成员为静态成员(属性、方法),静态成员在 Class 的原型上。

class Foo {
  static staticHandler() { }

  public instanceHandler() { }
}

// 解析后的JS代码
var Foo = /** @class */ (function () {
    function Foo() {
    }
    Foo.staticHandler = function () { };
    Foo.prototype.instanceHandler = function () { };
    return Foo;
}());

override 关键字,可以重写基类(Base)中的成员,如果基类中没有对应的成员则会报错。

class Base {
  printWithLove() { }
}

class Derived extends Base {
  override print() { // 此处会报错,因为基类中没有此方法
    // ...
  }
}

继承、实现、抽象类

Class 的类型包含以下几种:

  • 基类(Base):基础类没有继承任何父类
  • 派生类:通过继承的到
  • 抽象类:通过 abstract 关键字创建的类都属于抽象类,抽象类中可以有抽象方法,也可以有具体实现方法,但抽象类不可以被实例化
  • 抽象类的派生类:通过 implements 来对抽象类进行具体实现,且必须实现抽象类中的所有成员。
class Base { } // 基类

class Derived extends Base { } // 派生类

// 抽象类
abstract class Abstract {
	abstract print(): void;

	sayHello() {
		console.log('hello');
	}
}

// 抽象类的派生类
class Person implements Abstract {
	print(...arg: string[]): void {
		console.log(...arg);
	}
	sayHello(): void {
		console.log('hello');
	}
}

Day4

内置类型

内置类型包含以下几种:

  • any
  • never
  • unknown

类型层级 Top typeBottom type

简述大致的类型层级: any/unknown -> 原始类型、对象类型 -> 字面量类型 -> never

  • 最顶级的类型,any 与 unknown
  • 特殊的 Object ,它也包含了所有的类型,但和 Top Type 比还是差了一层
  • String、Boolean、Number 这些装箱类型
  • 原始类型与对象类型
  • 字面量类型,即更精确的原始类型与对象类型嘛,需要注意的是 null 和 undefined 并不是字面量类型的子类型
  • 最底层的 never

any

any 类型的主要意义,其实就是为了表示一个无拘无束的“任意类型”,它能兼容所有类型,也能够被所有类型兼容

any 的本质是类型系统中的顶级类型,即 Top Type,这是许多类型语言中的重要概念,我们会在类型层级部分讲解。

一旦对某个变量标注为 any 类型等于开挂,无视任何类型检查。在开发当中应该尽可能少的使用 any。

unknown

unknown 类型表示未知类型,也就是现在不确定它是什么类型,在使用时可以使用类型断言给予它一个确定的类型。

never

never 类型表示什么都没有,不携带任何类型信息

使用 never 类型作为函数返回值后在函数执行后的下方代码将被视为无法达到并不会被执行。

function ThrowError(): never {
    throw new Error('Error Message');
}

function foo() {
    ThrowError();
    
    // 在这以下的代码在编译器中变暗,即认为不会被执行。
    const a = 2; 
    console.log(a);
}

never 被称为 Bottom type,是整个类型系统层级中最底层的类型。只有 never 类型才能够赋值给另一个 never 类型变量。

declare let v1: never;
declare let v2: void;

v1 = v2; // X 类型 void 不能赋值给类型 never

v2 = v1;

类型断言

它的基本语法是 as NewType。你可以将 any / unknown 类型断言到一个具体的类型:

let unknownVar: unknown;

(unknownVar as { foo: () => {} }).foo();

也可以将联合类型断言一个具体的分支:

function foo(union: string | number) {
  if ((union as string).includes("linbudu")) { }

  if ((union as number).toFixed() === '599') { }
}

双重断言

断言类型与原类型差距过大时,需要使用 Top type 进行类型中转后再转换成断言类型,简述用法:

const str: string = "linbudu";

(str as unknown as { handler: () => {} }).handler();

// 使用尖括号断言
(<{ handler: () => {} }>(<unknown>str)).handler();

非空断言

非空断言其实是类型断言的简化,它使用 ! 语法,当遇到对象中可能不存在目标属性时使用非空断言,也就是剔除了(null 或 undefined)情况。例如:

declare const foo: {
  func?: () => ({
    prop?: number | null;
  })
};

foo.func().prop!.toFixed();

以上 prop 可能为 null,但使用非空断言后无论它有没有都认为它存在。