Bootstrap

学习TypeScript快速入门

🍁 作者主页:💖仙女不下凡💖

🍁 前言介绍:以下👇 内容是根据“阿宝哥”的教材自学总结,定期更新欢迎持续关注!

🍁 学习前提:学习该文章之前需要有一定JS的基础,较为熟悉ES5及ES6的相关知识基础!

🍁 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!


💚一、安装与运行

🔶1.1 安装

# 安装命令
$ npm install -g typescript

# 按指定版本号进行安装
$ npm install -g [email protected]

# 安装完毕后,查看版本号
$ tsc -v

🔶1.2 起步

# 创建文件夹
$ mkdir TypeScript

# 在TypeScript文件夹中创建demo.ts文件,其代码如下:
console.log('Hello,world')

🔶1.3 编译

正常编译

# 编译命令
$ tsc demo.ts
# 编译命令执行之后,生成demo.js文件

# 再执行,输出一下内容:Hello,world
$ node demo.js

安装插件之后直接启动浏览器编译,不安装插件不能直接编译。

# 安装ts-node
$ npm install ts-node -g

# 编译并执行
$ ts-node demo.ts

🔶1.4 TypeScript是什么

新建一个hello.ts文件,并输入一下内容:

 function handleInfo(person: string) {
   return 'Hello, ' + person;
 }

console.log(handleInfo(TS));

然后执行tsc hello.ts命令,之后会生成一个编译好的文件hello.js

"use strict"
function handleInfo(person) {
  return 'Hello, ' + person;
}

console.log(handleInfo(TS));

观察以上编译后的输出结果,我们发现person参数的类型信息在编译后被擦掉了。TS只会在编译阶段对类型进行静态检查,如果发现有错误,编译时就会报错、而运行时,编译生成的JS与普通的JavaScript文件一样,并不会进行类型检查。

💚二、TS泛型及应用

  • 原始数据类型包括:number、string、boolean、null、undefined和symbol。

🔶2.1 Number类型

let tsNum: number = 123
// ES5 var number = 123

🔶2.2 Boolean类型

let tsFlag: boolean = true
// ES5 var boolean = true

🔶2.3 String类型

let name: string = "semliker";
// ES5:var name = 'semlinker';

🔶2.4 Symbol类型

const sym = Symbol();
let obj = {
  [sym]: "semlinker",
}
console.log(obj[sym]); // semlinker

🔶2.5 Array类型

let list: number[] = [1, 2, 3];
// ES5 var list = [1, 2 ,3];

let list: Array<number>= [1, 2, 3]; // Array<number>泛型语法
// ES5 var list = [1, 2 ,3];

🔶2.6 Enum类型(进阶)

使用枚举我们可以定义一些带名字的常量。使用枚举可以清晰地表达意图或创建一组有区别的用例。TypeScript支持数字的和基于字符串的枚举

🔹2.6.1 数字枚举

enum Direction {
 NORTH,
 SOUTH,
 EAST,
 WEST,
}

let dir: Direction = Direction.NORTH;

默认情况下,NORTH的初始值为0,其余的成员会从1开始自动增长。换句话说Direction.SOUTH的值为1Direction.EAST的值为2Direction.WEST的值为3

以上的枚举示例编译后,对应的ES5代码如下:

"use strict"
var Direction;
(function (Direction) {
  Direction[(Direction ["NORTH"] = 0)] = "NORTH";
  Direction[(Direction ["SOUTH"] = 1)] = "SOUTH";
  Direction[(Direction ["EAST"] = 2)] = "EAST";
  Direction[(Direction ["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));

var dir = Direction.NORTH;

当然我们也可以设置NORTH的初始值,比如:

enum Direction {
 NORTH = 3,
 SOUTH,
 EAST,
 WEST,
}
let dir: Direction = Direction.NORTH;

🔹2.6.2 字符串枚举

TypeScript2.4版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Direction {
 NORTH = "NORTH",
 SOUTH = "SOUTH",
 EAST = "EAST",
 WEST = "WEST",
}
let dir: Direction = Direction.NORTH;

以上的枚举示例编译后,对应的ES5代码如下:

"use strict"
var Direction;
(function (Direction) {
  Direction ["NORTH"] = "NORTH";
  Direction ["SOUTH"] = "SOUTH";
  Direction ["EAST"] = "EAST";
  Direction ["WEST"] = "WEST";
})(Direction || (Direction = {}));

var dir = Direction.NORTH;

通过观察数字枚举和字符串枚举的编译结果,我们可以知道数字枚举除了支持 从成员名称到成员值 的普通映射之外,它还支持 从成员值到成员名称 的反向映射,如下:

enum Direction {
 NORTH,
 SOUTH,
 EAST,
 WEST,
}

let dirName = Direction[0]; // NORTH
let dir = Direction["NORTH"]; // 0

另外,对于纯字符串枚举,我们不能省略任何初始化程序。而数字枚举如果没有显式设置值时,则会使用默认规则进行初始化。

🔹2.6.3 常量枚举

除了数字枚举和字符串枚举之外,还有一种特殊的枚举 —— 常量枚举。它是使用const关键字修饰的枚举,常量枚举会使用内联语法,不会为枚举类型编译生成任何JavaScript。为了更好地理解这句话,我们来看一个具体的例子:

enum Direction {
 NORTH,
 SOUTH,
 EAST,
 WEST,
}
let dir: Direction = Direction.NORTH;

以上的枚举示例编译后,对应的ES5代码如下:

"use strict";
var dir = 0 /* NORTH */;

🔹2.6.4 异构枚举

异构枚举的成员值是数字和字符串的混合:

enum Enum {
  A,
  B,
  C = "C",
  D = "D",
  E = 8,
  F,
}

以上的枚举示例编译后,对应的ES5代码如下:

"use strict";
var Enum;
(function (Enum) {
  Enum[Enum["A"] = 0] = "A"
  Enum[Enum["B"] = 1] = "B"
  Enum[Enum["C"] = "C"
  Enum[Enum["D"] = "D"
  Enum[Enum["E"] = 8] = "E"
  Enum[Enum["F"] = 9] = "F"
  
})

通过观察上述代码生成ES5代码,我们可以发现数字枚举相对字符串枚举多了“反向映射”:

console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A

🔶2.7 Any类型

TypeScript中,任何类型都可以被归为any类型。这让any成为了类型系统的顶级类型(也被称作全局超级类型)。

any类型本质上是类型系统的一个逃逸仓,作为开发者,这给我们很大的自由:TypeScript允许我们对any类型的值执行任何操作,而无需事先执行任何形式的检查。比如:

let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

在许多场景下,这太宽松了。使用any类型,可以很容易地编写类型正确但有运行时有问题的代码。如果我们使用any类型,就无法使用TypeScript提供的大量的保护机制。为了解决any带来的问题,TypeScript 3.0引入了unknown类型。

🔶2.8 unknown类型

就像所有类型都可以赋值给any,所有类型也都可以赋值给unknown。这使得unknown成为TypeScript类型系统的另⼀种顶级类型(另⼀种是any)。

🔶2.9 Tuple类型(元组)

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在JavaScript中是没有元组的,元组是TypeScript中特有的类型,其工作方式类似于数组。

元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型、使用元组时,必须提供每个属性的值。为了更直观地理解元组的概念,我们来看一个具体的例子:

let tupleType: [string, boolean];
tupleType = ["semlinker", true];

在上面代码中,我们定义了一个名为tupleType的变量,它的类型是一个类型数组[string, boolean],然后我们按照正确的类型依次初始化tupleType变量。与数组一样,我们可以通过下标来访问元组中的元素:

console.log(tupleType[0]); // semlinker
console.log(tupleType[1]); // true

在元组初始化的时候,如果出现类型不匹配的话,⽐如:

let tupleType: [string, boolean];
tupleType = [true, "semlinker"];

此时,TypeScript 编译器会提示以下错误信息:

[0]: Type ‘true’ is not assignable to type ‘string’.
[1]: Type ‘string’ is not assignable to type ‘boolean’.

很明显是因为类型不匹配导致的。在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现
错误,⽐如:

let tupleType: [string, boolean];
tupleType = ["semlinker"];

此时,TypeScript 编译器会提示以下错误信息:

Property ‘1’ is missing in type ‘[string]’ but required in type ‘[string, boolean]’.

🔶2.10 Void类型

在JavaScript中,是没有空值(void)的概念的,某种成都上来说void类型像是与any类型相反,它表示没有任何类型,但在TypeScript中,当⼀个函数没有返回值时,你通常会⻅到其返回值类型是void

function sayHello (): void {
  console.log('Hello, world')
}

以上代码编译⽣成的 ES5 代码如下:

"use strict";
function sayHello () {
  console.log('Hello, world')
}

也可以定义一个void类型的变量,不过这样的变量没有什么意义,因为我们只能给这种变量赋值为null或undefined。

let voidValue1: void = null
let voidValue2: void = undefined

需要注意的是,声明⼀个 void 类型的变量没有什么作⽤,因为在严格模式下,它的值只能为undefined

🔶2.11 void、null和undefined区别

  • TypeScript中,nullundefined是所有类型的子类型,也就是说可以把undefinednull赋值给number等类型的变量,而对于void而言,它只能被赋值为null或者undefined
let tsNumber1: number = undefined
let tsNumber2: number = null

// 这两行代码会编译报错
let voidValue1: void = 123
let voidValue2: void = '123'

🔶2.12 object, Object 和 {} 类型

🔹2.12.1 object类型

object类型是:TypeScript 2.2引入的新类型,它用于表示非原始类型。

// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
 create(o: object | null): any;
 // ...
}

const proto = {};

Object.create(proto); // OK
Object.create(null); // OK
Object.create(undefined); // Error
Object.create(1337); // Error
Object.create(true); // Error
Object.create("oops"); // Error

🔹 2.12.2 Object类型

Object类型:它是所有Object类的实例的类型,它由一下两个接口来定义:

  • Object接口定义了Object.prototype原型对象上的属性;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
 constructor: Function;
 toString(): string;
 toLocaleString(): string;
 valueOf(): Object;
 hasOwnProperty(v: PropertyKey): boolean;
 isPrototypeOf(v: Object): boolean;
 propertyIsEnumerable(v: PropertyKey): boolean;
}
  • ObjectConstructor接口定义了Object类的属性。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
 /** Invocation via `new` */
 new(value?: any): Object;
 /** Invocation via function calls */
 (value?: any): any;
 readonly prototype: Object;
 getPrototypeOf(o: any): any;
 // ···
}

declare var Object: ObjectConstructor;

Object类的所有实例都继承了Object接⼝中的所有属性。

🔹 2.12.3 {}类型

{}类型描述了一个没有成员的对象。当你试图访问这样一个对象的任意属性时,TypeScript会产生一个编译时错误。

// Type {}
const obj = {};

// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";

但是,你仍然可以使⽤在Object类型上定义的所有属性和⽅法,这些属性和⽅法可通过JavaScript的原
型链隐式地使⽤, 如下:

// Type {}
const obj = {};

// "[object Object]"
obj.toString();

🔶2.13 Never类型

never类型表示的是哪些用不存在的值的类型。例如,never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头表达式的返回值类型。

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

function infiniteLoop(): never {
 while (true) {}
}

TypeScript中,可以利用never类型的特性来实现全面性检查,具体示例如下:

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
 if (typeof foo === "string") {
   // 这⾥ foo 被收窄为 string 类型
 } else if (typeof foo === "number") {
   // 这⾥ foo 被收窄为 number 类型
 } else {
   // foo 在这⾥是 never
   const check: never = foo;
 }
}

注意在else分支里面,我们把收窄为neverfoo赋值给一个显示声明的never变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了Foo的类型:

type Foo = string | number | boolean;

然而他忘记同时修改controlFlowAnalysisWithNever方法中的控制流程,这时候else分支的foo类型会被收窄为boolean类型,导致无法赋值给never类型,这时就会产生一个编译错误。通过这个方式,我们可以确保controlFlowAnalysisWithNever方法总是穷尽了Foo的所有可能类型。通过这个示例,我们可以得出结论:使用never避免了出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码

💚三、TypeScript断言

🔶3.1 类型断言

有时候你会遇到这种情况,你会比TypeScript更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种形式:

🔹3.1.1 “尖括号”语法

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

🔹3.1.2 as语法

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

🔶3.2 非空断言

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符!可以用于断言操作对象是非null和非undefined类型。具体而言,x!将从x值域中排除nullundefined

那么非空断言操作符到底有什么用呢?下面我们先来看一下非空断言操作符的一些适用场景。

🔹3.2.1 忽略undefined和null类型

function myFunc(maybeString: string | undefined | null) {
 // Type 'string | null | undefined' is not assignable to type 'string'.
 // Type 'undefined' is not assignable to type 'string'.
 const onlyString: string = maybeString; // Error
 const ignoreUndefinedAndNull: string = maybeString!; // Ok
}

🔹3.2.2 调用函数时忽略undefined类型

type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
 // Object is possibly 'undefined'.(2532)
 // Cannot invoke an object which is possibly 'undefined'.(2722)
 const num1 = numGenerator(); // Error
 const num2 = numGenerator!(); //OK
}

因为!非空断言操作符会从编译生成的JavaScript代码中移除,所以在实际使用的过程中,要特别注意。比如下面这个例子:

const a: number | undefined = undefined;
const b: number = a!;
console.log(b);

以上TS代码会编译生成一下ES5代码:

"use strict";
const a = undefined;
const b = a;
console.log(b);

虽然在TS代码中,我们使用了非空断言,使得const b: number = a!;语句可以通过TypeScript类型检查器。但在生成的ES5代码中,!非空断言操作符被移除了,所以在浏览在这里插入代码片器中执行以上代码,在控制台会输出undefined

🔶3.3 确定赋值断言

TypeScript 2.7版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个!号,从而告诉TypeScript该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子:

let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
 x = 10;
}

通过let x!: number;确定赋值断⾔,TypeScript编译器就会知道该属性会被明确地赋值。

💚四、类型守卫

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。

目前主要有四种的方式来实现类型保护:

🔶4.1 in关键字

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Admin | Employee

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}

🔶4.2 typeof关键字

function padLeft(value: string, padding: string | number) {
 if (typeof padding === "number") {
 return Array(padding + 1).join(" ") + value;
 }
 if (typeof padding === "string") {
 return padding + value;
 }
 throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof类型保护只支持两种形式:typeof v === "typename"typeof v !== typenametypename必须是numberstringbooleansymbol。但是TypeScript并不会阻止你与其他字符串比较,语言不会把那些表达式识别为类型保护。

🔶4.3 instanceof关键字

instanceof Padder {
 getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
 constructor(private numSpaces: number) {}
 getPaddingString() {
   return Array(this.numSpaces + 1).join(" ");
 }
}

class StringPadder implements Padder {
 constructor(private value: string) {}
 getPaddingString() { 
   return this.value;
 }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
 // padder的类型收窄为 'SpaceRepeatingPadder'
}

🔶4.4 ⾃定义类型保护的类型谓词

function isNumber(x: any): x is number {
 return typeof x === "number";
}

function isString(x: any): x is string {
 return typeof x === "string";
}

💚 五、联合类型和类型别名

🔶5.1 联合类型

联合类型通常与nullundefined一起使用:

const sayHello = (name: string | undefined) => {
 /* ... */
};

例如,这里name的类型是string | undefined意味着可以将stringundefined的值传递给sayHello函数。

sayHello('sayHello');
sayHello(undefined);

通过这个示例,你可以凭直觉知道类型A和类型B联合后的类型是同时接受AB值的类型。此外,对于联合类型来说,你可能会遇到以下的用法:

let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove'

以上示例中的12click被称为字⾯量类型,⽤来约束取值只能是某⼏个值中的⼀个。

🔶5.2 可辨识联合

TypeScript可辨识联合类型,也称为代数数据类型或标签联合类型。它包含3个要点:可辨识、联合类型和类型守卫

这种类型的本质是结合联合类型和字面量类型的一种类型保护方式。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块

🔹5.2.1 可辨识

可辨识要求联合类型中每个元素都含有一个单例类型属性,比如:

enum CarTransmission {
 Automatic = 200,
 Manual = 300
}

interface Motorcycle {
 vType: "motorcycle"; // discriminant
 make: number; // year
}

interface Car {
 vType: "car"; // discriminant
 transmission: CarTransmission
}

interface Truck {
 vType: "truck"; // discriminant
 capacity: number; // in tons
}

在上述代码中,我们分别定义了MotorcycleCarTruck三个接口,在这些接口中都包含一个vType属性,该属性被称为可辨识的属性,而其他的属性只跟特性的接口相关。

🔹5.2.2 联合类型

基于前面定义了三个接口,我们可以创建一个Vehicle联合类型:

type Vehicle = Motorcycle | Car | Truck

现在我们就可以开始使用Vehicle联合类型,对于Vehicle类型的变量,它可以表示不同类型的车辆。

🔹5.2.3 类型守卫

下面我们来定义一个evaluatePrice方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:

const EVALUATION_FACTOR = Math.PI;

function evaluatePrice(vehicle: Vehicle) {
 return vehicle.capacity * EVALUATION_FACTOR;
}

const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);

对于以上代码,TypeScript编译器将会提示以下错误信息:

Property ‘capacity’ does not exist on type ‘Vehicle’.
Property ‘capacity’ does not exist on type ‘Motorcycle’.

原因是Motorcycle接口中,并不存在capacity属性,而对于Car接口来说,它也不存在capacity属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的evaluatePrice方法,重构后的代码如下:

function evaluatePrice(vehicle: Vehicle) {
 switch(vehicle.vType) {
 case "car":
 return vehicle.transmission * EVALUATION_FACTOR;
 case "truck":
 return vehicle.capacity * EVALUATION_FACTOR;
 case "motorcycle":
 return vehicle.make * EVALUATION_FACTOR;
 }
}

在以上代码中,我们使⽤switchcase运算符来实现类型守卫,从⽽确保在evaluatePrice⽅法中,我们可以安全地访问vehicle对象中的所包含的属性,来正确的计算该⻋辆类型所对应的价格。

🔶5.3 类型别名

类型别名⽤来给⼀个类型起个新名字。

type Message = string | string[];

let greet = (message: Message) => {
 // ...
};

💚六、交叉类型

TypeScript中交叉类型是将多个类型合并为一个类型。通过&运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

let point: Point = {
 x: 1,
 y: 1
}

在上⾯代码中我们先定义了PartialPointX类型,接着使⽤&运算符创建⼀个新的Point类型,表示⼀个含有 x 和 y 坐标的点,然后定义了⼀个Point类型的变量并初始化。

🔶6.1 同名基础类型属性的合并

那么现在问题来了,假设在合并多个类型的过程中,刚好出现某些类型存在相同的成员,但对应的类型
又不⼀致,⽐如:

interface X {
 c: string;
 d: string;
}

interface Y {
 c: number;
 e: string
}

type XY = X & Y;
type YX = Y & X;

let p: XY;
let q: YX;

在上⾯的代码中,接⼝X和接⼝Y都含有⼀个相同的成员c,但它们的类型不⼀致。对于这种情况,此
XY类型或YX类型中成员c的类型是不是可以是stringnumber类型呢?比如下⾯的例⼦:

p = { c: 6, d: "d", e: "e" };
// 报错
p = { c: 'c', d: "d", e: "e" };
// 报错

为什么接⼝XY接⼝混⼊后,成员c的类型会变成never呢?
这是因为混⼊后成员c的类型string&number,即成员c的类型既可以是string类型⼜可以是number类型。很明显这种类型是不存在的,所以混⼊后成员c的类型为never

🔶6.2 同名非基础类型属性的合并

在上面示例中,刚好接口X和接口Y中内部成员c的类型都是基本数据类型,那么如果是非基本数据类型的话,又会是什么情形。我们来看个具体的例子:

interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }

interface A { x: D; }
interface B { x: E; }
interface C { x: F; }

type ABC = A & B & C;

let abc: ABC = {
 x: {
   d: true,
   e: 'semlinker',
   f: 666
 }
};
console.log('abc:', abc);// ok

由以上可知,在混⼊多个类型时,若存在相同成员,且成员类型为非基本数据类型,那么是可以成功合并。

💚七、TypeScript函数

🔶7.1 TypeScript函数与JavaScript函数的区别

TypeScriptJavaScript
含有类型无类型
箭头函数箭头函数(ES2015)
函数类型无函数类型
必填和可选参数所有参数都是可选的
默认参数默认参数
剩余参数剩余参数
函数重载无函数重载

🔶7.2 箭头函数

🔹7.2.1 常见语法

myBooks.forEach(() => console.log('reading'));

myBooks.forEach(title => console.log(title));

myBooks.forEach((title, idx, arr) =>
 console.log(idx + '-' + title);
);

myBooks.forEach((title, idx, arr) => {
 console.log(idx + '-' + title);
});

🔹7.2.2 使用示例

// 未使⽤箭头函数
function Book() {
 let self = this;
 self.publishDate = 2016;
 setInterval(function () {
   console.log(self.publishDate);
 }, 1000);
}

// 使⽤箭头函数
function Book() {
 this.publishDate = 2016;
 setInterval(() => {
   console.log(this.publishDate);
 }, 1000);
}

🔶7.3 参数类型和返回类型

function createUserId(name: string, id: number): string {
 return name + id;
}

🔶7.4 函数类型

let IdGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
 return name + id;
}

IdGenerator = createUserId;

🔶7.5 可选参数及默认参数

// 可选参数
function createUserId(name: string, id: number, age?: number): string {
 return name + id;
}

// 默认参数
function createUserId(
 name = "semlinker",
 id: number,
 age?: number
): string {
 return name + id;
}

在声明函数时,可以通过?号来定义可选参数,比如age?: number这种形式。在实际使用时,需要注意时是可选参数要放在普通参数的后面,不然会导致编译错误

🔶7.6 剩余参数

function push(array, ...items) {
 items.forEach(function (item) {
   array.push(item);
 });
}

let a = [];
push(a, 1, 2, 3);

🔶7.7 函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
 // type Combinable = string | number;
 if (typeof a === 'string' || typeof b === 'string') {
   return a.toString() + b.toString();
 }
 return a + b;
}

在以上代码中,我们为add函数提供了多个函数类型定义,从而实现函数的重载。在TypeScript中除了可以重载普通函数之外,我们还可以重载类中的成员方法。

方法重载是指在同一类中方法同名,参数不同(参数类型不同,参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:

class Calculator {
 add(a: number, b: number): number;
 add(a: string, b: string): string;
 add(a: string, b: number): string;
 add(a: number, b: string): string;
 add(a: Combinable, b: Combinable) {
 if (typeof a === 'string' || typeof b === 'string') {
   return a.toString() + b.toString();
 }
 return a + b;
 }
}

const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');

这里需要注意的是,当TypeScript编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。如果匹配的话就是使用这个。因此,在定义重载的时候,一定要把最精准的定义放在最前面。另外在Calculator类中,add(a: Combinable, b: Combinable) {}并不是重载列表的一部分,因此对于add成员方法来说,我们只定义了四个重载方法。

💚八、TypeScript数组

🔶8.1 数组解构

let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;

🔶8.2 数组展开运算符

let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];

🔶8.3 数组遍历

let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
 console.log(i);
}

💚九、TypeScript对象

🔶9.1 对象解构

let person = {
 name: "Semlinker",
 gender: "Male",
};

let { name, gender } = person;

🔶9.2 对象展开运算符

let person = {
 name: "Semlinker",
 gender: "Male",
 address: "Xiamen",
};

// 组装对象
let personWithAge = { ...person, age: 33 };

// 获取除了某些项外的其它项
let { name, ...rest } = person;

💚十、TypeScript接口

在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。TypeScript中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对【对象的形状(Shape)】进行描述。

🔶10.1 对象的形状

interface Person {
 name: string;
 age: number;
}
let semlinker: Person = {
 name: "semlinker",
 age: 33,
};

🔶10.2 可选 | 只读属性

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

只读属性用于限制只能在对象刚刚创建的时候修改其值。此外TypeScript还提供了ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a; // 另一种写法:let ro: readonly number[] = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

🔶10.3 任意属性

有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这是我们可以使用索引签名的形式来满足上诉要求。

interface Person {
 name: string;
 age?: number;
 [propName: string]: any;
}
const p1 = { name: "semlinker" };
const p2 = { name: "lolo", age: 5 };
const p3 = { name: "kakuqo", sex: 1 }

🔶10.4 接口与类型别名的区别

🔹10.4.1 Objects/Functions

接口和类型别名都可以⽤来描述对象的形状或函数签名:

接口

interface Point {
 x: number;
 y: number;
}

interface SetPoint {
 (x: number, y: number): void;
}

类型别名

type Point = {
 x: number;
 y: number;
};

type SetPoint = (x: number, y: number) => void;

🔹10.4.2 Other Types

与接口类型不⼀样,类型别名可以⽤于⼀些其他类型,比如原始类型、联合类型和元组:

// primitive
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

🔹10.4.3 Extend关键字继承

接口和类型别名都能够被扩展,但语法有所不同。此外,接口和类型别名不是互斥的。接口可以扩展类型别名,而反过来是不行的

Interface extends interface

interface PartialPointX { x: number; }
interface Point extends PartialPointX {
 y: number;
}

Type alias extends type alias

type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

Interface extends type alias

type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

Type alias extends interface

interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };

以上个人总结,使用接口interface声明的类型与使用type类型别名声明的类型,主要是区别是interface需要使用extend继承扩展,type使用&或者|等继承扩展。

🔹10.4.4 Implements

类可以以相同的方式实现接口或类型别名,但类不能实现使用类型别名定义的联合类型:

interface Point {
 x: number;
 y: number;
}

class SomePoint implements Point {
 x = 1;
 y = 2;
}

type Point2 = {
 x: number;
 y: number;
};

class SomePoint2 implements Point2 {
 x = 1;
 y = 2;
}

type PartialPoint = { x: number; } | { y: number; };

// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
 x = 1;
 y = 2;
}

🔹10.4.5 Declaration merging合并

与类型别名不同,接口可以定义多次,会被自动合并为当个接口。

interface Point { x: number; }
interface Point { y: number; }

const point: Point = { x: 1, y: 2 };

💚十一、TypeScript类

🔶11.1 类的属性与方法

在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。

TypeScript中,我们可以通过Class关键字来定义一个类:

class Greeter {
 // 静态属性
 static cname: string = "Greeter"; // static 是类修饰符,具体解释不在本文章范围
 // 成员属性
 greeting: string;
 
 // 构造函数 - 执⾏初始化操作
 constructor(message: string) {
   this.greeting = message;
 }
 
 // 静态⽅法
 static getClassName() {
   return "Class name is Greeter";
 }
 
 // 成员⽅法
 greet() {
   return "Hello, " + this.greeting;
 }
}

let greeter = new Greeter("world");

那么成员属性与静态属性,成员方法与静态方法有什么区别呢?这里无需过多解释,我们直接看一下编译生成的ES5代码:

"use strict";
var Greeter = /** @class */ (function () {
    // 构造函数 - 执⾏初始化操作
    function Greeter(message) {
        this.greeting = message;
    }
    // 静态⽅法
    Greeter.getClassName = function () {
        return "Class name is Greeter";
    };
    // 成员⽅法
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    // 静态属性
    Greeter.cname = "Greeter"; // static 是类修饰符,具体解释不在本文章范围
    return Greeter;
}());

var greeter = new Greeter("world");

🔶11.2 ECMAScript 私有字段🐻‍❄️

TypeScript 3.8版本就开始支持ECMAScript私有字段,使用方式如下:

class Person {
 #name: string;
 constructor(name: string) {
 this.#name = name;
 }
 greet() {
 console.log(`Hello, my name is ${this.#name}!`);
 }
}

let semlinker = new Person("Semlinker");

semlinker.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

与常规属性(甚至使用)…

🔶11.3 访问器:getter/setter

TypeScript中,我们可以通过gettersetter方法来实现数据的封装和有效性校验,防止出现异常数据。

let passcode = "Hello TypeScript";

class Employee {
  private _fullName: string;
  get fullName(): string {
    return this._fullName;
  }
  set fullName(newName: string) {
    if (passcode && passcode == "Hello TypeScript") {
      this._fullName = newName;
    } else {
    console.log("Error: Unauthorized update of employee!");
    }
  }
}

let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
 console.log(employee.fullName);
}

🔶11.4 类的继承

继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。

TypeScript中,我们可以通过extends关键字来实现继承:

class Animal {
  name: string;
  constructor(theName: string) {
    this.name = theName;
  }
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Snake extends Animal {
  constructor(name: string) {
    super(name); //ES6 要求,子类的构造函数必须执行一次super函数
  }
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

let sam = new Snake("Sammy the Python");
sam.move();

🔶11.5 抽象类

使用abstract关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法。所谓的抽象方法,是指不包含具体实现的方法:

abstract class Person {
 constructor(public name: string){}
 abstract say(words: string) :void;
}

// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error

抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类。具体如下所示:

abstract class Person {
  constructor(public name: string){}
  // 抽象⽅法
  abstract say(words: string) :void;
}

class Developer extends Person {
  constructor(name: string) {
    super(name);
  }
  say(words: string): void {
    console.log(`${this.name} says ${words}`);
  }
}

const lolo = new Developer("lolo");
lolo.say("I love ts!"); // lolo says I love ts!

🔶11.6 类方法重载

在前面的章节,我们已经介绍了函数重载。对于类的方法来说,它也支持重载。比如,在以下示例中我们重载了ProductService类的getProducts成员方法:

class ProductService {
  getProducts(): void;
  getProducts(id: number): void;
  getProducts(id?: number) {
    if(typeof id === 'number') {
      console.log(`获取id为 ${id} 的产品信息`);
    } else {
      console.log(`获取所有的产品信息`);
     } 
  }
}

const productService = new ProductService();
productService.getProducts(666); // 获取id为 666 的产品信息
productService.getProducts(); // 获取所有的产品信息

💚十二、TypeScript泛型

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重要性。组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这时创建大型系统时为你提供了十分灵活的功能。

在像C#Java这样的语⾔中,可以使用泛型来创建可重用的组件,⼀个组件可以支持多种类型的数
据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键⽬的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函
数参数和函数返回值。

泛型(Generics)是允许同⼀个函数接受不同类型参数的⼀种模板。相比于使用any类型,使用泛型来
创建可复用的组件要更好,因为泛型会保留参数类型。

🔶12.1 泛型语法

对于刚接触TypeScript泛型的读者来说,首次看到<T>语法会感到陌生。其实它没有什么特别,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。
泛型语法
参考上面的图片,当我们调用identity<Number>(1)Number类型就像参数1一样,它将在出现T的任何位置填充该类型。图中<T>内部的T被称为类型变量,它是我们希望传递给identity函数的类型占位符,同时它被分配给value参数用来代替它的类型:此时T充当的是类型,而不是特定的Number类型。

其中T代表Type,在定义泛型时通常用作第一个类型变量名称。但实际上T可以用任何有效名称代替。除了T之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型U,用于扩展我们定义的identity函数:

function identity <T, U>(value: T, message: U) : T {
 console.log(message);
 return value;
}

console.log(identity<Number, string>(68, "Semlinker"));

泛型语法-引入新的类型
除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

console.log(identity(68, "Semlinker"));

对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给TU,而不需要开发人员显式指定它们。

🔶12.2 泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}

🔶12.3 泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

🔶12.4 泛型工具类型

为了方便开发者TypeScript内置了一些常用的工具类型,比如PartialRequiredReadonlyRecordReturnType等。

🔹12.4.1 typeof

TypeScript中,typeof操作符可以用来获取一个变量声明或对象的类型。

interface Person {
 name: string;
 age: number;
}
const sem: Person = { name: 'semlinker', age: 33 };
type Sem= typeof sem; // -> Person


function toArray(x: number): Array<number> {
 return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]

🔹12.4.2 keyof

keyof操作符是在TypeScript 2.1版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

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

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number

TypeScript 中支持两种索引签名,数字索引和字符串索引:

interface StringArray {
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string;
}

interface StringArray1 {
  // 数字索引 -> keyof StringArray1 => number
  [index: number]: string;
}

为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。**其中的原因就是当使用数字索引时,JavaScript在执行索引操作时,会先把数字索引先转换为字符串索引。**所以keyof { [x: string]: Person }的结果会返回string | number

🔹12.4.3 in

in用来遍历枚举类型:

type Keys = "a" | "b" | "c"

type Obj = {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

🔹12.4.4 infer🐻‍❄️

在条件类型语句中,可以用infer声明一个类型变量并且对他进行使用。

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

以上代码中infer R就是声明一个变量来继承传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

🔹12.4.5 extends

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过extends关键字添加泛型结束。

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3); // Error, number doesn't have a .length property

这时我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3}); // ok

🔹12.4.6 Partial

Partial<T>的作用就是将某个类型里的属性全部变为可选项?
定义:

/**
 * node_modules/typescript/lib/lib.es5.d.ts
 * Make all properties in T optional
 */
type Partial<T> = {
 [P in keyof T]?: T[P];
};

在以上代码中,首先通过keyof T拿到T的所有属性名,然后使用in进行遍历,将值赋给P21,最后通过T[P]取得响应相应的属性值。中间的?号,用于将所有属性变为可选。
示例:

interface Todo {
  title: string;
  description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
  title: "Learn TS",
  description: "Learn TypeScript",
};
const todo2 = updateTodo(todo1, {
  description: "Learn TypeScript Enum",
});

在上面的updateTodo方法中,我们利用Partial<T>工具类型,定义fieldsToUpdate的类型为Partial<Todo>,即:

{
 title?: string | undefined;
 description?: string | undefined;
}

💚十三、TypeScript装饰器

🔶13.1 装饰器是什么

  • 它是一个表达式
  • 该表达式被执行后,返回一个函数
  • 函数的入参分别为targetnamedescriptor
  • 执行该函数后,可能返回descriptor对象,用于配置target对象

🔶13.2 装饰器的分类

  • 类装饰器(Class decorators
  • 属性装饰器(Property decorators
  • 方法装饰器(Method decorators
  • 参数装饰器(Parameter decorators

需要注意的是,若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:
命令行:

tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

🔶13.3 类装饰器

类装饰器声明:

🔶

🔶

🔶

下面没更新完,会在本周更新完毕

💚十四、TypeScript 4.0新特性

🔶

🔶

💚十五、编译上下文

🔶

🔶

🔶

💚十六、TypeScript开发辅助⼯具

🔶

🔶

🔶

🔶

🔶

🔶

🔶

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;