TypeScript 基本语法

TypeScript

开发规范参考:https://juejin.cn/post/7047843645273145358

简介

认识

TypeScript 是一种给 JavaScript 添加特性的语言扩展。增加的功能包括:

  • 类型批注和编译时类型检查
  • 类型推断
  • 类型擦除
  • 接口
  • 枚举
  • Mixin
  • 泛型编程
  • 名字空间
  • 元组
  • Await

TypeScript 作为 JavaScript 的超集,他在拥有 JavaScript 所有能力的基础上,做到了静态类型,这解决了 JavaScript 编写不规范会导致的很多问题,虽然它依旧是弱类型语言,但它在变量进行类型隐式转换时也做了限制,让它在编写的过程中可以更加规范。而不规范的地方,会直接报错来提醒开发者。

https://markdown-1303167219.cos.ap-shanghai.myqcloud.com/ts-2020-11-26-1.png

TypeScript 的类型推导是基于上下文类型推导(Contextual Typing)的。所以即便你不添加任何类型,编译器也会根据编写的代码,推导出变量或函数的类型,当无法推导出的时候,TypeScript 则将其设置为any

1
2
const hello : string = "Hello World!"
console.log(hello)

TS 与 JS 的区别

TypeScriptJavaScript
JavaScript 的超集用于解决大型项目的代码复杂性一种脚本语言,用于创建动态网页。
可以在编译期间发现并纠正错误作为一种解释型语言,只能在运行时发现错误
强类型,支持静态和动态类型弱类型,没有静态类型选项
最终被编译成 JavaScript 代码,使浏览器可以理解可以直接在浏览器中使用
支持模块、泛型和接口不支持模块,泛型或接口
支持 ES3,ES4,ES5 和 ES6 等不支持编译其他 ES3,ES4,ES5 或 ES6 功能
社区的支持仍在增长,而且还不是很大大量的社区支持以及大量文档和解决问题的支持

安装

针对使用 npm 的用户:

1
2
3
npm install -g typescript
# or
npm i -g typescript

使用命令来执行 ts 代码:

1
2
tsc file1.ts
node file.js

https://markdown-1303167219.cos.ap-shanghai.myqcloud.com/ts-2020-11-26-3.png

简化执行步骤

安装 ts-node

1
npm i -g ts-node

执行

1
ts-node f

基础类型

我们使用 let 关键字来代替大家所熟悉的 JavaScript 关键字 varlet 关键字是 JavaScript 的一个新概念,TypeScript 实现了它。很多常见的问题都可以通过使用 let 来解决,所以尽可能地使用 let 来代替 var 吧。

布尔值 boolean

1
let isDone: boolean = false;

数字 number

和 JavaScript 一样,TypeScript 里的所有数字都是浮点数

1
2
3
4
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;

字符串 string

1
2
let name: string = "bob";
name = "smith";

数组 list

两种方式可以定义数组。 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:

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

第二种方式是使用数组泛型,Array<元素类型>

1
let list: Array<number> = [1, 2, 3];

元组 tuple

元组类型允许表示一个已知元素数量和类型的数组(各元素的类型不必相同)

1
2
3
4
5
6
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

1
2
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素,会使用联合类型替代:

1
2
3
4
5
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型

console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString

x[6] = true; // Error, 布尔不是(string | number)类型

枚举 enum

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

1
2
enum Color {Red, Green, Blue}
let c: Color = Color.Green;

指定编号

默认情况下,从 0 开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1 开始编号:

1
2
3
4
5
6
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

// 或者,全部都采用手动赋值:
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

字符串枚举

在 TypeScript 2.4 版本,允许我们使用字符串枚举。

在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

1
2
3
4
5
6
enum Direction {
  NORTH = "NORTH",
  SOUTH = "SOUTH",
  EAST = "EAST",
  WEST = "WEST",
}

任意值 any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。

这种情况下,我们不希望类型检查器对这些值进行检查。 那么我们可以使用 any 类型来标记这些变量:

1
2
3
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

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

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

1
2
3
4
5
6
7
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 类型。

当你只知道一部分数据的类型时,any类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:

1
2
3
let list: any[] = [1, true, "free"];

list[1] = 100;

unknown

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

value 变量的所有赋值都被认为是类型正确的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK

unknown 类型只能被赋值给 any 类型和 unknown 类型本身:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

对类型为 unknown 的值执行操作是非法的:

1
2
3
4
5
6
7
let value: unknown;

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。

空值 void

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void

1
2
3
function warnUser(): void {
    alert("This is my warning message");
}

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefinednull

1
let unusable: void = undefined;

Null 和 Undefined

TypeScript 里,undefinednull 两者各自有自己的类型分别叫做 undefinednull

void 相似,它们的本身的类型用处不是很大…

1
2
3
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

默认情况下 nullundefined 是所有类型的子类型。 就是说你可以把他们赋值给 number 类型的变量

然而,当你指定了 --strictNullChecks 标记,nullundefined只能赋值给 void 和它们各自。 这能避免很多常见的问题。

也许在某处你想传入一个 stringnullundefined,你可以使用联合类型 string | null | undefined

never

never 类型表示的是那些永不存在的值的类型

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

变量也可能是 never 类型,当它们被永不为真的类型保护所约束时。

never 类型是任何类型的子类型,也可以赋值给任何类型

然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
    return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}

类型断言

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

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

类型断言有两种形式

“尖括号” 语法

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

as 语法

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

类型守卫

A type guard is some expression that performs a runtime check that guarantees the type in some scope.

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。

换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。

类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:

in 关键字

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
interface Admin {
  name: string;
  privileges: string[];
}

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

type UnknownEmployee = Employee | Admin;

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);
  }
}

typeof 关键字

1
2
3
4
5
6
7
8
9
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 !== typename

"typename" 必须是 "number""string""boolean""symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

instanceof 关键字

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface 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'
}

自定义类型保护的类型谓词

1
2
3
4
5
6
7
function isNumber(x: any): x is number {
  return typeof x === "number";
}

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

接口 interface

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。

在 TypeScript 里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue 接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label 属性且类型为string 的对象,上述代码等同于:

1
2
3
4
5
6
function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。

类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。

可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

在可选属性名字定义的后面加一个 ? 符号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
  let newSquare = {color: "white", area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({color: "black"});

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 可以在属性名前用 readonly 来指定只读属性

1
2
3
4
5
6
7
interface Point {
    readonly x: number;
    readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

TypeScript 具有 ReadonlyArray<T> 类型,它与 Array<T> 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:

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

ReadonlyArray 不可赋值。要做到这点,可以用类型断言重写:

1
a = ro as number[];

最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用 readonly

额外的属性检查

对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候。

如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
    // ...
}

// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });

要绕开这些检查, 最简便的方法是使用类型断言

1
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

但最佳的方式是能够添加一个字符串索引签名(前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性):

1
2
3
4
5
interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量

1
2
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

因为 squareOptions 不会经过额外属性检查,所以编译器不会报错

要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。

函数类型

除了描述带有属性的普通对象外,接口也可以描述函数类型

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

1
2
3
4
5
6
7
8
9
interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
}

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配:

1
2
3
4
5
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
}

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的

如果你不想指定类型,TypeScript 的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc 类型变量

1
2
3
4
5
let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}

可索引的类型

与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如 a[10]ageMap["daniel"]

可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型

1
2
3
4
5
6
7
8
interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

共有支持两种索引签名:字符串和数字

可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。 也就是说用 100(一个 number)去索引等同于使用 "100"(一个 string)去索引,因此两者需要保持一致。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

// 错误:使用'string'索引,有时会得到Animal!
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

字符串索引签名能够很好的描述 dictionary 模式,并且它们也会确保所有属性与其返回值类型相匹配,因为字符串索引声明了 obj.propertyobj["property"] 两种形式都可以

下面的例子里,name 的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

1
2
3
4
5
interface NumberDictionary {
  [index: string]: number;
  length: number;    // 可以,length是number类型
  name: string       // 错误,`name`的类型与索引类型返回值的类型不匹配
}

可以将索引签名设置为只读,这样就防止了给索引赋值:

1
2
3
4
5
6
interface ReadonlyStringArray {
    readonly [index: number]: string;
}

let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

类类型

实现接口

与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。

1
2
3
4
5
6
7
8
interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

可以在接口中描述一个方法,在类里实现它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员

联合类型和类型别名

联合类型

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

1
2
3
4
5
6
const sayHello = (name: string | undefined) => {
  /* ... */
};

sayHello("Semlinker");
sayHello(undefined);

可辨识联合

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

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

可辨识

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。

联合类型

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

1
type Vehicle = Motorcycle | Car | Truck;

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

类型守卫

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

1
2
3
4
5
6
7
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 编译器将会提示以下错误信息:

1
2
Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。

类型别名

类型别名用来给一个类型起个新名字

1
2
3
4
5
type Message = string | string[];

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

交叉类型

TypeScript 交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface IPerson {
  id: string;
  age: number;
}

interface IWorker {
  companyId: string;
}

type IStaff = IPerson & IWorker;

const staff: IStaff = {
  id: 'E1006',
  age: 33,
  companyId: 'EFT'
};

console.dir(staff)

函数

与 JS 函数的区别

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

箭头函数

常见语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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);
});

使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 未使用箭头函数
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);
}

参数类型和返回类型

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

函数类型

1
2
3
4
5
6
7
let IdGenerator: (chars: string, nums: number) => string;

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

IdGenerator = createUserId;

搞不懂有啥用。。

可选参数及默认参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 可选参数
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}

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

在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。

剩余参数

1
2
3
4
5
6
7
8
function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

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

函数重载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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) {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString();
  }
  return a + b;
}

在定义重载的时候,一定要把最精确的定义放在最前面

数组

数组解构

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

数组展开运算符

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

数组遍历

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

对象

对象解构

1
2
3
4
5
6
let person = {
  name: "Semlinker",
  gender: "Male",
};

let { name, gender } = person;

对象运算符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let person = {
  name: "Semlinker",
  gender: "Male",
  address: "Xiamen",
};

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Greeter {
  // 静态属性
  static cname: string = "Greeter";
  // 成员属性
  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");

类的静态成员存在于类本身上面而不是类的实例上。

访问器

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Hello{
    private _name: string;
    private _age: number;
    get name(): string {
        return this._name;
    }
    set name(value: string) {
        this._name = value;
    }
    get age(): number{
        return this._age;
    }
    set age(age: number) {
        if(age>0 && age<100){
            console.log("年龄在0-100之间"); // 年龄在0-100之间
            return;
        }
        this._age = age;
    }
}

let hello = new Hello();
hello.name = "muyy";
hello.age = 23
console.log(hello.name); // muyy

另外一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}

继承

通过 extends 关键字来实现继承:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
  }
  
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

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

ECMAScript 私有字段

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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.

与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:

  • 私有字段以 # 字符开头,有时我们称之为私有名称;
  • 每个私有字段名称都唯一地限定于其包含的类;
  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
  • 私有字段不能在包含的类之外访问,甚至不能被检测到。

泛型

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

泛型与模板类似

泛型接口

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

泛型类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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;
};

范型变量

一些常见泛型变量代表的意思:

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

泛型工具类型

typeof

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
interface Person {
  name: string;
  age: number;
}

const sem: Person = { name: 'semlinker', age: 30 };
type Sem = typeof sem; // -> Person


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

keyof

keyof 操作符可以用来一个对象中的所有 key 值:

1
2
3
4
5
6
7
8
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

in

in 用来遍历枚举类型:

1
2
3
4
5
type Keys = "a" | "b" | "c"

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

infer

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

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

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

extends

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

1
2
3
4
5
6
7
8
interface ILengthwise {
  length: number;
}

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

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

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

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

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

Partial

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

定义:

1
2
3
4
5
6
7
/**
 * 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 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface Todo {
  title: string;
  description: string;
}

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

const todo1 = {
  title: "organize desk",
  description: "clear clutter",
};

const todo2 = updateTodo(todo1, {
  description: "throw out trash",
});

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

1
2
3
4
{
   title?: string | undefined;
   description?: string | undefined;
}

装饰器

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

类装饰器

声明

1
2
3
declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

类装饰器用来装饰类,它接收一个参数:

  • target: TFunction:被装饰的类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function Greeter(target: Function): void {
  target.prototype.greet = function (): void {
    console.log("Hello Semlinker!");
  };
}

@Greeter
class Greeting {
  constructor() {
    // 内部实现
  }
}

let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello Semlinker!';

上面的例子中,我们定义了 Greeter 类装饰器,同时我们使用了 @Greeter 语法糖,来使用装饰器。

还可以在装饰器中加入参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function Greeter(greeting: string) {
  return function (target: Function) {
    target.prototype.greet = function (): void {
      console.log(greeting);
    };
  };
}

@Greeter("Hello TS!")
class Greeting {
  constructor() {
    // 内部实现
  }
}

let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello TS!';

属性装饰器

声明

1
2
declare type PropertyDecorator = (target:Object, 
  propertyKey: string | symbol ) => void;

属性装饰器用来装饰类的属性。它接收两个参数:

  • target: Object:被装饰的类
  • propertyKey: string | symbol:被装饰类的属性名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function logProperty(target: any, key: string) {
  delete target[key];

  const backingField = "_" + key;

  Object.defineProperty(target, backingField, {
    writable: true,
    enumerable: true,
    configurable: true
  });

  // property getter
  const getter = function (this: any) {
    const currVal = this[backingField];
    console.log(`Get: ${key} => ${currVal}`);
    return currVal;
  };

  // property setter
  const setter = function (this: any, newVal: any) {
    console.log(`Set: ${key} => ${newVal}`);
    this[backingField] = newVal;
  };

  // Create new property with getter and setter
  Object.defineProperty(target, key, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Person { 
  @logProperty
  public name: string;

  constructor(name : string) { 
    this.name = name;
  }
}

const p1 = new Person("semlinker");
p1.name = "kakuqo";

以上代码我们定义了一个 logProperty 函数,来跟踪用户对属性的操作,当代码成功运行后,在控制台会输出以下结果:(晕晕的,暂时难以接受。。)

1
2
Set: name => semlinker
Set: name => kakuqo

方法装饰器

声明

1
2
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol, 	 	
  descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

方法装饰器用来装饰类的方法。它接收三个参数:

  • target: Object:被装饰的类
  • propertyKey: string | symbol:方法名
  • descriptor: TypePropertyDescript:属性描述符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function LogOutput(tarage: Function, key: string, descriptor: any) {
  let originalMethod = descriptor.value;
  let newMethod = function(...args: any[]): any {
    let result: any = originalMethod.apply(this, args);
    if(!this.loggedOutput) {
      this.loggedOutput = new Array<any>();
    }
    this.loggedOutput.push({
      method: key,
      parameters: args,
      output: result,
      timestamp: new Date()
    });
    return result;
  };
  descriptor.value = newMethod;
}

class Calculator {
  @LogOutput
  double (num: number): number {
    return num * 2;
  }
}

let calc = new Calculator();
calc.double(11);
// console ouput: [{method: "double", output: 22, ...}]
console.log(calc.loggedOutput); 

参数装饰器

声明

1
2
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, 
  parameterIndex: number ) => void

参数装饰器用来装饰函数参数,它接收三个参数:

  • target: Object:被装饰的类
  • propertyKey: string | symbol:方法名
  • parameterIndex: number:方法中参数的索引值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function Log(target: Function, key: string, parameterIndex: number) {
  let functionLogged = key || target.prototype.constructor.name;
  console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
	been decorated`);
}

class Greeter {
  greeting: string;
  constructor(@Log phrase: string) {
	this.greeting = phrase; 
  }
}

// console output: The parameter in position 0 
// at Greeter has been decorated

编译上下文

tsconfig.json 的作用

  • 用于标识 TypeScript 项目的根路径;
  • 用于配置 TypeScript 编译器;
  • 用于指定编译的文件。

tsconfig.json 重要字段

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions - 设置与编译流程相关的选项。

compilerOptions 选项

compilerOptions 支持很多选项,常见的有 baseUrltargetbaseUrlmoduleResolutionlib 等。

compilerOptions 每个选项的详细说明如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

与 Vue3 的结合

ref 的类型声明

1
2
const year = ref(2020)
const month = ref<string | number>('9')

如果不给定 ref 定义的类型的话, vue3 也能根据初始值来进行类型推导,然后需要指定复杂类型的时候简单传递一个泛型即可。

reactive 的类型声明

1
2
3
4
5
const student = reactive<Student>({ name: '阿勇', age: 16 })
// or
const student: Student = reactive({ name: '阿勇', age: 16 })
// or
const student = reactive({ name: '阿勇', age: 16, class: 'cs' }) as Student

比较倾向于第一种

自定义 hooks

作者:JS 开发宝典 链接:https://zhuanlan.zhihu.com/p/360947847 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

vue3 借鉴 react hooks 开发出了 Composition API ,那么也就意味着 Composition API 也能进行自定义封装 hooks ,接下来我们就用 TypeScript 风格封装一个计数器逻辑的 hooksuseCount ):

首先来看看这个 hooks 怎么使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { ref } from '/@modules/vue'
import  useCount from './useCount'

export default {
  name: 'CountDemo',
  props: {
    msg: String
  },
  setup() {
    const { current: count, inc, dec, set, reset } = useCount(2, {
      min: 1,
      max: 15
    })
    const msg = ref('Demo useCount')

    return {
      count,
      inc,
      dec,
      set,
      reset,
      msg
    }
  }
}

出来的效果就是:

贴上 useCount 的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { ref, Ref, watch } from 'vue'

interface Range {
  min?: number,
  max?: number
}

interface Result {
  current: Ref<number>,
  inc: (delta?: number) => void,
  dec: (delta?: number) => void,
  set: (value: number) => void,
  reset: () => void
}

export default function useCount(initialVal: number, range?: Range): Result {
  const current = ref(initialVal)
  const inc = (delta?: number): void => {
    if (typeof delta === 'number') {
      current.value += delta
    } else {
      current.value += 1
    }
  }
  const dec = (delta?: number): void => {
    if (typeof delta === 'number') {
      current.value -= delta
    } else {
      current.value -= 1
    }
  }
  const set = (value: number): void => {
    current.value = value
  }
  const reset = () => {
    current.value = initialVal
  }

  watch(current, (newVal: number, oldVal: number) => {
    if (newVal === oldVal) return
    if (range && range.min && newVal < range.min) {
      current.value = range.min
    } else if (range && range.max && newVal > range.max) {
      current.value = range.max
    }
  })

  return {
    current,
    inc,
    dec,
    set,
    reset
  }
}

分析源码

这里首先是对 hooks 函数的入参类型和返回类型进行了定义,入参的 Range 和返回的 Result 分别用一个接口来指定,这样做了以后,最大的好处就是在使用 useCount 函数的时候,ide 就会自动提示哪些参数是必填项,各个参数的类型是什么,防止业务逻辑出错。

https://pic2.zhimg.com/v2-b3bffccd67b28b5f41413189b0fc47c1_b.jpg

接下来,在增加 inc 和减少 dec 的两个函数中增加了 typeo 类型守卫检查,因为传入的 delta 类型值在某些特定场景下不是很确定,比如在 template 中调用方法的话,类型检查可能会失效,传入的类型就是一个原生的 Event

关于 ref 类型值,这里并没有特别声明类型,因为 vue3 会进行自动类型推导,但如果是复杂类型的话可以采用类型断言的方式: ref(initObj) as Ref<ObjType>