这篇文章是 Obsidian Dataview 系列 系列 15 篇文章中的第 6 篇

目录

一、类型

1. 为什么需要类型

在一个多人协作的大型项目中,小明作为基础库的研发工程师,提供了一个公共函数调用 showInput(input),定义如下:

function showInput(input) {
  return input.show();
}

此时,作为业务调用方的小红,在业务代码中作出调用:

// 在javascript中调用报错
// Uncaught TypeError: input.show is not a function
showInput("hello world");

看过前面的函数定义,我们知道以上代码不出意外会报错,因为 hello world字符串没有show方法

现实情况中,showInput(input) 可能是我们可以了解的此API的所有外形描述。函数定义对于调用者而言是一个黑盒,错误的调用成为了大概率事件,可怕的是开发阶段你完全觉察不到错误的存在,把存在错误的代码发布到生产环境,可能会导致业务损失。

为了解决这个问题,我们用typescript为 input 参数提供了类型描述 InputType

// InputType描述了一个包含show方法的类型
type InputType = {
  show: ()=>void;
};
showInput(input: InputType);

当你用同样的方式在 typescript 中调用时 showInput("hello world"),在代码没有发布之前,编译器就会提示你存在错误

// 以下代码在typescript中调用报错
// error TS2345: Argument of type '"hello world"' is not assignable to parameter of type 'InputType'.
showInput("hello world");

在支持typescript语言的编辑器(如vscode)中,InputType 类型甚至会在编码阶段就被正确的智能提示出来,改成正确版本:

// 定义input变量,包含show方法
const input = {
  show(){
    return "hello world"
  }
}
// 将包含show的变量传递给函数
showInput(input);

这是到目前为止,这是JavaScript需要强制类型的最好理由,它可以让绝大部分的错误发生在编码阶段,而不是让错误发生在线上

2. 值的类型化

(1)什么是值

简单点说,凡是可以被变量存储的单元都是值。它可以是简单的字符串、数字,也可以是复杂的由多条语句组成的代码单元,如类、函数、对象等:

// 字符串作为值
let stringValue = 'hello world';

// 对象字面量作为值
let objectLiteralValue = {
  attribute: 'hello world'
};

// 函数作为值
let funcValue = function(){};

// 函数返回作为值
function fn(){
  return 'hello world';
}
let returnValue = fn();

// 类作为值
let classValue = class {}

在TypeScript中,所有的值都具有强制类型,值的类型化是TypeScript区别于JavaScript最显著的特征。

实际上,JavaScript也是有类型的,只不过JavaScript的类型信息是在编译阶段由编译器判定,对程序员来说,值可以任意赋予,编写代码时就好像类型弱的不存在一样

(2)类型注解

类型注解是指在源代码中显式指定值的类型,它的语法格式如下:

: 类型

冒号加类型构成了类型注解,冒号前后可以包含任意空格,以下都是合法的类型注解语法:

// 声明字符串类型变量
let str: string;

// 声明字符串类型变量并初始化
let strValue: string = 'hello world';

// 指定类的属性类型
class Hello {
  show: boolean = true;
}

// 指定函数参数和返回值类型
function sum(a: number, b: number): number {
  return a + b;
}

下表列举了TypeScript支持的所有类型,作为一个学习前的概览,在后续章节将会详细介绍:

类型 语法形式
数字类型 number
布尔类型 boolean
字符串类型 string
符号类型 symbol
Void类型 void
Null类型 null
Undefined类型 undefined
Never类型 never
任意类型 any
数组类型 T[]
元组类型 [T0, T1, ...]
枚举类型 enum T { ... }
函数类型 (p1: T1, p2: T2, ...) => T
类类型 T
构造器类型 new (p1: T1, p2: T2, ...) => R
对象类型 { ... } 或 interface T { ... }
联合类型 `T1
交叉类型 T1 & T2 & ...

(3)类型推导

日常开发中,类型注解是可选的,在绝大多数未显式注解类型的情况下,编译器能自动推导出值的类型

[!tip] 小技巧:如果你在编码时用肉眼能看出一个未注解值的类型,那么编译器也能推导出来

// 变量被自动推导为字符串类型 string
let variable = 'hello world';
// 等价于
let variable: string = 'hello world';

// 返回值被自动推导为数字类型 number
function show(param: number) {
  return param;
}
// 等价于
function show(param: number): number {
  return param;
}

(4)类型查询

类型查询是一条语句,相当于一个独立类型。代码中任何需要显式注解类型的地方,都可以使用类型查询代替:

: typeof 值

在JavaScript中,typeof 是一个用来判断值类型的关键字,在类型查询语法中有类似的作用。

// 声明a为number
let a: number;

// 通过类型查询声明b的类型
let b: typeof a;
// 等价于
let b: number;

// 函数fn为函数
function fn(){}

// 通过类型查询声明d的类型为fn的类型
let d: typeof fn;
// 等价于
let d: () => void;

(5)总结

类型注解类型推导类型查询构成了TypeScript的类型判定系统,TypeScript编译器判定值的类型时,主要是通过以上三种方式。

3. 简单类型

(1)数字类型

数字类型关键字为 number ,所有数字都是浮点数

let intLiteral: number = 6;
let floatLiteral: number = 3.1415;

(2)布尔类型

布尔类型关键字为 boolean,仅有两个值 true 和 false

let isDone: boolean = false;
isDone = true;

(3)字符串类型

字符串类型关键字为 string

let name: string = "bob";

(4)符号类型

符号类型关键字为 symbol

let sym: symbol = Symbol();  
let sym1: symbol = Symbol('test');

(5)Void类型

Void类型关键字为 void , Void类型 表示没有类型或空类型。

当一个函数没有返回值时,你可以显式指定返回值为 void;如果不显式指定,会被自动推导为 Void类型

// 显式指定返回类型为 void
function hi(): void { /**函数体为空**/ }
// 和上面等价,返回值会被自动推导为 void
function hi() { /**函数体为空**/ }

显式声明一个值为 Void类型 是合法的,但没什么意义,因为你只能为它赋值undefined 或 null

let x: void = undefined;

// 仅在 strictNullChecks 编译选项关闭时合法
let y: void = null; 

[!tip] 当把 null 赋值给 Void类型 的时候,仅在 strictNullChecks 编译选项关闭时才合法

(6)Null类型 和 Undefined类型

  • Null类型关键字为 null
  • Undefined类型关键字为 undefined

[!tip] null 和 undefined 既可以是类型关键字,也可以是值,参靠下面示例代码

Null类型和Undefined类型的类型表现和编译选项 strictNullChecks 有关,上面已经提到这个选项。编译相关的教程后面会有专门的介绍,此处不做讲解。

strictNullChecks 选项打开
  • null 只能赋值给 Null类型
  • undefined 只能赋值给 Undefined类型 和 Void类型
// 合法
let n: null = null;
let u: undefined = undefined;
let v: void = undefined;

// 不合法
let v: void = null;
strictNullChecks 选项关闭
  • null 和 undefined 可以赋值给任意类型
// 合法
let v1: undefined = null;
let v2: void = undefined;
let v3: null = undefined;
let x: number = null;
let y: string = undefined;

(7)Never类型

Never类型关键字为 never。Never类型代表不可能存在的类型,Never类型常常伴随着错误和异常出现(因为错误和异常可以提前终止,让值无法到达):

// 抛出异常,永远无法返回
function error(message: string): never {
    throw new Error(message);
}

// 死循环,永远无法返回
function infiniteLoop(): never {
    while (true) {
    }
}

// 声明交叉类型变量:既是布尔类型也是数字类型
let n: boolean & number;
// 以下赋值皆错误,不可能存在一个值既是布尔类型也是数字类型
n = 1; // error TS2322: Type '1' is not assignable to type 'never'
n = null; //  error TS2322: Type 'null' is not assignable to type 'never'
n = false; // error TS2322: Type 'false' is not assignable to type 'never'

上面示例中的交叉类型变量赋值,根据错误提示可知,会被编译器推导为 never 类型:

// 声明不可能存在的交叉类型
let n: boolean & number;
// 等价于
let n: never;

(8)任意值类型

任意值的类型关键字为 any,任意值就是可以被当做任意类型使用的值。任意值具有动态类型特性:

// 声明为任意类型
let notSure: any = 4;

// 赋值为字符串类型
notSure = "hello world";

// 赋值为布尔类型
notSure = false;

一个值,如果满足以下任何一个条件,会被判定为任意类型:

1、显式指定类型 any

// 显式指定变量类型,x为any
let x: any;

// 显式指定变量类型,y为any
let y: any = 10;

2、未显式指定类型,且没有初始化或默认值

// 未指定类型,且未初始化。x为any
let x;

// 未指定类型,但初始化了。y被自动推导为number
let y = 10;

// 未指定类型,且没有默认值。参数x为any
function f(x) {
  console.log(x);
}

尽量不要使用any类型,any类型表现上跟JavaScript的类型一样弱。TypeScript最大的优势在于类型化带来的强约束作用,他可以让你更早的发现错误,并带来更高的可维护性

4. 数组类型

(1)数组类型

数组类型的语法为:

T[]

其中 T 可以是任何类型,代表的是数组的元素类型

let list: number[] = [1, 2, 3];
// 二维数组
let vec: number[][] = [[1, 2, 3], [1, 2, 3]];

(2)元组类型

元组和数组类似,只不过元组是一种固定长度的数组,每个元素有自己的类型。元组(Tuple)的语法为:

[T0, T1, ...]

T0, T1代表任意类型,省略号表示可以有任意多个元素。

// 声明一个元组,包含两个元素,第一个元素为string类型,第二个元素为number类型
let x: [string, number];

// 正确
x = ['hello', 10];

// 错误;元素类型不匹配
// error TS2322: Type 'number' is not assignable to type 'string'
// error TS2322: Type 'string' is not assignable to type 'number'
x = [10, 'hello'];

// 错误;长度不匹配
// error TS2741: Property '1' is missing in type '[string]' but required in type '[string, number]'
x = ['hello'];

5. 函数类型

在JavaScript中,函数可以作为一个整体赋值给一个变量:

// javascript代码
let fn = function(){}

既然函数可以作为值赋给变量,那么函数就应该有自己的类型。上述代码用TypeScript等价表示如下:

// ()=>void 是变量fn的类型,代表是一个函数类型
let fn: () => void;
fn = function(){}

上例中,() => void 就是变量 fn 的类型注解,也就是函数类型。

(1)语法:

// 这里的箭头=>用于类型声明
(p1: T1, p2: T2, ...) => T

(2)函数定义

在TypeScript中,函数定义相对于JavaScript的区别就是可以显式的为参数和返回值添加类型注解

// javascript函数定义
function sum(a, b) {
  return a + b;
}

// 改为typescript函数定义
function sum(a: number, b: number): number {
  return a + b;
}

直接定义的函数有自己的隐式函数类型,比如对于上述函数定义

function sum(a: number, b: number): number {
  return a + b;
}

编译器会推导出 sum 函数具有类型:

(a: number, b: number) => number;

通过类型查询,可以获取一个直接定义的函数的类型:

// 直接定义的函数
function sum(a: number, b: number): number {
  return a + b;
}

// 通过typeof关键字获取函数sum的类型为:
// (a: number, b: number) => number;
let fn: typeof sum;

// 将类型兼容的函数赋值给fn
fn = function(x: number, y: number): number {
  return x + y;
}

(3)类型兼容

判断一个函数类型是否和一个函数兼容,只需判断参数类型和返回值类型是否同时兼容:

// 声明fn为函数类型
let fn: (x: number, y: string) => boolean;

// 正确,参数名字不做兼容检查
fn = function(a: number, b: string): boolean {
  // ...
  return true;
}

// 正确,允许值的参数比声明参数少
fn = function(a: number): boolean {
  // ...
  return true;
}

// 错误,函数返回值类型不匹配
// error TS2322: Type '(a: number, b: string) => string' is not assignable to type '(x: number, y: string) => boolean'
fn = function(a: number, b: string): string {
  // ...
  return b;
}

// 错误,参数过多
// error TS2322: Type '(a: number, b: string, c: number) => boolean' is not assignable to type '(x: number, y: string) => boolean
fn = function(a: number, b: string, c: number): boolean {
  // ...
  return true;
}

// 错误,参数类型不匹配,第二个参数应该为 string 类型
// error TS2322: Type '(a: number, b: boolean) => boolean' is not assignable to type '(x: number, y: string) => boolean'
fn = function(a: number, b: boolean): boolean {
  // ...
  return true;
}

// 错误,返回值类型不匹配,应该为 boolean
// error TS2322: Type '(a: number, b: string) => void' is not assignable to type '(x: number, y: string) => boolean'
fn = function(a: number, b: string): void {
  // ...
}

(4)可选参数

可选参数在调用时可以不必传参;而必选参数在调用时必须传参。在参数的类型注解前添加 ? 即可让参数成为可选参数:

// 参数a必须,参数b可选,b必须位于a之后
function test(a: number, b?: number): void {
  // ...
}

// 正确,忽略可选参数
test(1);
// 正确,传递可选参数
test(1, 2);

// 错误,必选参数a未传递
// error TS2554: Expected 1-2 arguments, but got 0
test();

必须注意的是可选参数必须位于必选参数之后,下面的定义就是不合法的:

// 错误,可选参数之后不能有必选参数
// error TS1016: A required parameter cannot follow an optional parameter
function test(a?: number, b: number): void {
  // ...
}

6. 枚举类型

通过以下语法可以定义新的枚举类型:

enum T { ... }

其中 T 是任意定义的名字,省略号 ... 表示可以定义一个或多个可以显式初始化的枚举值。如:

enum Direction {
  Up,   // 值默认为 0
  Down, // 值默认为 1
  Left, // 值默认为 2
  Right // 值默认为 3
}

上述语句定义了新的枚举类型关键字 Direction,现在可以用这个关键字声明新的枚举类型:

// 声明d为枚举类型Direction
let d: Direction;

定义枚举类型的时候就限制了枚举的取值范围,上述定义中枚举类型有4个值 Direction.UPDirection.DownDirection.LeftDirection.Right,枚举类型在任一时刻只能取这4个值其中之一

// 声明并初始化
let d: Direction = Direction.Down;
// 改变枚举类型的值
d = Direction.Up;

(1)枚举值的默认值和自增性

如果没有对枚举值显式初始化,那么枚举值默认为数字类型,第一个出场的元素默认为数字 0,后续未显式初始化的枚举值会在前一个数字值的基础上自动 +1。根据规则,可以得出前面的枚举定义中:

Direction.Up === 0;
Direction.Down === 1;
Direction.Left === 2;
Direction.Right === 3;

如果我们为枚举定义显式添加数字默认值

enum Direction {
  Up = 2,
  Down,
  Left = 3.3,
  Right
}

则得出:

Direction.Up === 2;       // 显式初始化
Direction.Down === 3;     // 2 + 1 = 3
Direction.Left === 3.3;   // 显式初始化
Direction.Right === 4.3;  // 3.3 + 1 = 4.3

在上述场景中,枚举值实际上就是数字类型 number,此时可以将枚举值直接赋值给数字:

let n: number  = Direction.Right; // 合法

(2)作为字符串的枚举值

除了初始化为数字,还可以将枚举值显式初始化为字符串:

enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT'
}

// 正确
Direction.Up === 'UP'; 
Direction.Down === 'DOWN';     
Direction.Left === 'LEFT';  
Direction.Right === 'RIGHT'; 

// 枚举值是字符串,直接赋值给字符串类型
let s: string = Direction.Up;  

在初始化为字符串的场景中,值的数字自增性依然起作用。如果一个没有显式初始化的枚举值前面是一个字符串,将会报错:

enum Direction {
  Up = 'UP',
  Down, // error TS1061: Enum member must have initializer
  Left, // error TS1061: Enum member must have initializer
  Right // error TS1061: Enum member must have initializer
}

虽然枚举值 Direction.Down 没有显式初始化,但自增引擎仍然会起作用:编译器检测到前面一个出场的值 Direction.Up 为字符串类型,无法自增 +1,此时编译器报错。改正这个错误很简单:

enum Direction {
  Up = 'UP',
  Down = 3,
  Left, // 不会报错,值为 3+1=4
  Right // 不会报错,值为 4+1=5
}

// 正确
Direction.Up === 'UP'; 
Direction.Down === 3;     
Direction.Left === 4;  
Direction.Right === 5; 

7. 复合类型

(1)交叉类型

语法
T1 & T2 & ...

交叉类型是将多个类型合并为一个总的类型,它包含了多个类型的所有特性,类似于编程逻辑中的  操作

interface Bird {
  fly(): void;
}
interface Dog {
  run(): void;
} 

// 同时具有Bird的fly和Dog的run特征
class Animal {
  fly(){}
  run(){}
}

// 正确
let animal: Bird & Dog = new Animal();

(2)联合类型

语法
T1 | T2 | ...

联合类型是取多个类型中的其中之一,只要满足了其中一个类型,就认为类型兼容。联合类型类似于编程逻辑中的  操作

interface Bird {
  fly(): void;
}
interface Dog {
  run(): void;
} 

// 与 Bird 兼容
class Animal1 {
  fly(){}
}
// 与 Dog 兼容
class Animal2 {
  run(){}
}

// 正确,满足了Bird
let animal1: Bird | Dog = new Animal1();
// 正确,满足了Dog
let animal2: Bird | Dog = new Animal2();

(3)高级联合

联合类型非常神奇,构成联合类型的组合可以是以下三种方式的任意组合

  • 值与类型
  • 值与值
  • 类型与类型
// 值与类型混搭
let u: 99 | string | boolean;
u = 'a';
u = 99;
u = true;

// 值与值混搭
let u: 'a' | 'b' | 99;
u = 'a';
u = 'b';
u = 99;

// 类型与类型混搭
let u: number | string | boolean;
u = 99;
u = 'hello world';
u = false;

(4)keyof 关键字

语法

// T代表类型
keyof T

keyof 关键字作用于类型,通过获取一个类型的所有属性名,生成一个新的字符串值的联合类型:

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

// 通过keyof关键字声明联合
let keyWord: keyof Person;
// 完全等价于
let keyWord: "name" | "age";

keyWord = "name";
keyWord = "age";

也就是说,keyof 关键字生成的是字符串值的联合,每个字符串值都是被获取类型 T 的键的索引名

8. 接口类型

(1)对象类型

对象类型是TypeScript的类型系统中最复杂也是最重要的类型,对象类型主要用来描述复杂数据类型:

// 声明一个值为对象字面量
let man = {name: 'joye', age: 30};

// 等价于
let man: {name: string; age: number} = {name: 'joye', age: 30};

在上例第一条语句中,实际上变量 man 会被自动推导为类型 {name: string; age: number},它描述了一个对象具有字符串类型的 name 属性和数字类型的 age 属性 。

(2)接口类型

对象类型是匿名的接口类型,对象类型没有名字,接口类型有名字。接口类型相当于为对象类型声明了一个别名

// 定义接口类型Person
interface Person {
  name: string;
  age: number;
}

// 声明变量 man 为 Person 接口类型
let man: Person = {name: 'joye', age: 30};

上述语句完全等价于:

let man: {name: string; age: number} = {name: 'joye', age: 30};

(3)可选属性

接口的属性是可选的,可选属性类似于函数的可选参数:属性名后添加问号?即可

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

// 错误,缺少必选属性 age
// error TS2322: Type '{ name: string; }' is not assignable to type 'Person'. 
// Property 'age' is missing in type '{ name: string; }'.
let man: Person = {
  name: 'joye'
};

将 age 改成可选属性:

interface Person {
  name: string;
  // 注意此处的问号,age此时为可选属性
  age?: number; 
}

// 正确
let man: Person = {
  name: 'joye'
};

(4)只读属性

可以通过在属性名前添加 readonly 关键字来指定只读属性,只读属性只能在创建的时候对其赋值,一旦创建完成,就再也不能更改:

interface Person {
  // 声明name为只读
  readonly name: string;
  age: number;
}
// 创建时对只读属性赋值
let man: Person = {
  name: 'joye',
  age: 30
}

// 错误,只读属性的值不能更改
// error TS2540: Cannot assign to 'name' because it is a constant or a read-only property
man.name = 'mike';

// 正确,非只读属性的值可以更改
man.age = 31;

(5)接口的应用

接口最重要的作用在于描述一个复杂值的外形,通常情况下,接口可以描述:

  • 对象字面量
  • 函数
  • 可索引值
描述对象字面量

前面的 Person 接口就是描述对象字面量的例子,此处不再重复举例。

对象字面量的类型匹配非常让人迷惑,请看下面的例子:

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

// 定义一个对象字面量male
let male = {
  name: 'joye',
  age: 30,
  gender: 'male'
};

// 正确,male包含Person接口的所有属性
let man: Person = male;

在上面的例子中,对象字面量 male 被编译器推导为匿名接口类型,相当于:

// 声明male为匿名接口
let male: {
    name: string;
    age: number;
    gender: string;
};
// 对male赋值
male = {
  name: 'joye',
  age: 30,
  gender: 'male'
};

匿名接口类型包含了 Person 接口的所有属性 nameage,编译器认为类型匹配,通过类型检查。然而:

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

// 直接将对象字面量赋值给接口类型
// 错误,对象字面量直接赋值检查所有属性的兼容性
// error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'. 
// Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
let man: Person = {
  name: 'joye',
  age: 30,
  gender: 'male'
};

请牢记:对象字面量在直接赋值的时候,编译器会检查字面量类型是否完全匹配,多一个或少一个属性都会报错。

描述函数

声明一个函数类型的多种方式:

// 描述函数
interface MyFunc {
  (name: string, age: number): string; 
}

// 声明接口类型
let fn: MyFunc; 
// 等价于
let fn: { (name: string, age: number): string; } // 匿名接口
// 等价于
let fn: (name: string, age: number) => string; 


// 赋值
fn = function(name: string, age: number): string {
  return `${name}, ${age}`;
}

例子,用接口描述一个带静态属性的函数:

// 定义myFunc 函数
function myFunc(){}
// myFunc具有静态属性 `test`
myFunc.test = 'hello world';

// 声明接口类型描述函数 myFunc
interface MyFunc {
  // 这条语句描述函数定义
  (): void;
  // 这条语句描述静态属性 `test`
  test: string;
}

// 正确,类型匹配
let newFunc: MyFunc = myFunc;

描述可索引值

可索引值一般表示数组类型和对象类型,可以通过键访问某一项的值或属性值。

描述数组:

// 描述一个数组
interface StringArray {
  [index: number]: string;
}

// 声明接口类型
let myArray: StringArray;
// 等价于
let myArray: { [index: number]: string; }; // 匿名接口
// 等价于
let myArray: string[];

// 赋值
myArray = ["Bob", "Fred"];

描述对象:

// 描述一个对象
interface MyObject {
  [index: string]: string;
}

// 声明接口类型
let myObject: MyObject;

// 赋值
myObject = {
  a: '1', b: '2', c: '3'
}

对比前面描述对象字面量的语法,你会发现,这种方式描述对象可以支持无限多的对象属性,上述例子中:

// 省略号代表其他任意属性
myObject = {
  a: '1', b: '2', c: '3', d: '4', ...
}

描述类数组对象

如果一个对象既支持数字索引,也支持字符串索引,这种对象在JavaScript中被称作类数组对象:

// 类数组对象
let obj = {
  1: 1,
  2: 2,
  name: 'joye',
  age: 30
}

obj[1] === 1;
obj[2] === 2;
obj['name'] === 'joye';
obj['age'] === 30;

实际上,当采用数字索引方式访问一个值时,JavaScript会将数字索引转换为字符串索引:

obj[1] === 1;
obj[2] === 2;

// 完全等价于
obj["1"] === 1;
obj["2"] === 2;

在TypeScript中,接口类型可以同时描述数字索引类型和字符串索引类型:

// 正确
interface IndexObj {
    [x: number]: string;
    [x: string]: string;
}

但要注意,由于JavaScript会将数字索引转换为字符串索引,数字索引和字符串索引的值的类型必须相等,或者数字索引的返回值必须是字符串索引返回值类型的子类型:

// 错误,数字索引的值和字符串索引的值不匹配
// error TS2413: Numeric index type 'number' is not assignable to string index type 'string'
interface IndexObj {
    [x: number]: number;
    [x: string]: string;
}

(6)描述类

在 类类型 章节的构造器类型讲解中可以知道,构造器类型代表的就是类本身。用接口来描述一个类:

// 定义一个类
class NewClass {}

// 用接口来描述这个类类型
interface MyClass {
  new(): NewClass;
}

// 声明一个变量为描述这个类的接口类型并初始化
let myClass: MyClass = NewClass;
// 等价于
let myClass: typeof NewClass = NewClass;

(7)用类来实现接口

我们介绍到用接口来描述函数、可索引值、类类型,你会发现还不如直接用类型来声明更直接:

// 声明函数
let myFunc: ()=>{};

// 声明数组
let myArr: string[];

// 声明类
class MyClass {}
let myClass: typeof MyClass;

在实际使用中的确是这样,我们很少直接用接口来声明一个函数或数组。接口更重要的场景在于可以被类实现,从而实现各种复杂的设计模式,在TypeScript中,接口可以被类实现

在面向对象的编程方法学中,接口对于代码可维护性和业务逻辑解耦起着至关重要的作用

// ClockInterface 描述了一个属性和一个方法
interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

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

实现类必须包含接口所声明的全部必选属性,在上面的例子中:Clock类必须同时包含属性 currentTime 和方法 setTime

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

// 错误,缺少属性 currentTime
// error TS2420: Class 'Clock' incorrectly implements interface 'ClockInterface'. 
// Property 'currentTime' is missing in type 'Clock'
class Clock implements ClockInterface {
  setTime(d: Date) {}
  constructor(h: number, m: number) { }
}

(8)接口继承

接口也可以互相继承,通过继承,子接口将继承父接口的成员:

interface Shape {
    color: string;
}
interface Square extends Shape {
    sideLength: number;
}

// 正确,color 属性来自父接口
let square: Square = {
  color: 'blue',
  sideLength: 4
};

9. 类类型

类本身就是一种类型,类的名字可以直接作为类型名:

// 定义类
class TypeA {
  // ...
}

// 声明TypeA类型
let a: TypeA;
// 赋值TypeA类型
a = new TypeA();

语法扩展

属性扩展

在ES6中,实例属性和静态属性不能直接定义在类内部(有新提案通过了可以在内部直接声明实例属性和静态属性的新写法):

// 下面是ES6代码

// 不合法的定义
class Greeter {
  // 错误,实例属性不能定义在类里
  greeting = 'world';
  // 错误,静态属性不能定义在类里
  static greeting = 'world';
}

// 合法的定义
class Greeter {
  constructor(){
    // 正确,ES6中实例属性只能定义在构造器内部
    this.greeting = 'world';
  }
}
// 正确,ES6中静态属性只能定义在类外部
Greeter.greeting = 'world';

而在TypeScript中,下面的定义是合法的:

// 下面是TypeScript代码

class Greeter {
  // 定义实例属性并初始化
  greeting: string = 'world';

  // 定义静态属性并初始化
  static greeting: string = 'world';
}
访问控制 public/private/protected 修饰符

在TypeScript中,修饰符不是必须的,他们主要用来控制类成员的可访问性,类成员包括:

  • 实例属性
  • 静态属性
  • 实例方法
  • 静态方法
  • 构造函数
  • getter/setter

如果你没有显式为成员添加访问控制修饰符,那么将默认为 public

class Animal {
  // name属性未显式添加修饰符,默认为public
  age: number; 

  // 显式添加 private
  private name: string;

  // show方法未显式添加修饰符,默认为public
  show(){ 
    // ...
  }
}

// 与上述定义等价
class Animal {
  public age: number;
  private name: string;
  public show() {
    // ...
  }
}

当成员被标记成 private 时,它不能在声明它的类外部访问。比如:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

// 错误: 不能在类外访问私有成员
// error TS2341: Property 'name' is private and only accessible within class 'Animal'
new Animal("Cat").name;

protected 修饰符与 private 修饰符的行为相似,都不能在类的外部访问。但有一点不同, protected 成员可以在派生类中访问。例如:

// 定义基类
class Person {
  protected name: string;
  constructor(name: string) { this.name = name; }
}

// 定义派生类
class Employee extends Person {
  private department: string;

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

  public getElevatorPitch() {
    // 正确,来自父类的name成员在派生类里可以访问。虽然它位于Persion类的外部
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());

// 错误,受保护的成员 name 不能在外部访问
// error TS2445: Property 'name' is protected and only accessible within class 'Person' and its subclasses
console.log(howard.name); 

访问控制修饰符可直接用于构造函数的参数声明,这是一个语法糖,可以非常方便的实现属性的定义并初始化:

class Animal {
  // private修饰符用于构造器的name参数前
  constructor(private name: string) {}
}

// 以上定义相当于
class Animal {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
}
只读修饰符 readonly

你可以使用 readonly 关键字将属性设置为只读的。 只读属性必须声明时构造函数里被初始化

class Octopus {
  readonly name: string;
  // 在属性声明时初始化
  readonly numberOfLegs: number = 8;
  constructor (theName: string) {
    // 在构造函数里初始化
    this.name = theName;
  }
}
let dad = new Octopus("Man with the 8 strong legs");

// 错误! name 是只读的
// error TS2540: Cannot assign to 'name' because it is a constant or a read-only property
dad.name = "Man with the 3-piece suit";

只读属性和常量类似,一旦初始化,之后就再也不允许被赋值。

抽象类和抽象方法

用于描述抽象类和抽象方法的关键字都是 abstract,抽象方法没有方法体(也就是不包含实现):

// Animal是抽象类
abstract class Animal {
  // makeSound是抽象方法,没有方法体
  abstract makeSound(): void;
  move(): void {
      console.log('roaming the earch...');
  }
}

一般情况下,抽象类和抽象方法是同时出现的,但也有例外:

  1. 一个类包含抽象方法,那么这个类必须是抽象类
  2. 抽象类可以没有抽象方法
class Animal {
  // 错误,有抽象方法,但是类不是抽象类,不符合[1]
  // error TS1244: Abstract methods can only appear within an abstract class
  abstract makeSound(): void;
  move(): void {
      console.log('roaming the earch...');
  }
}

// 正确,抽象类可以没有抽象方法,符合[2]
// 没有抽象方法的抽象类和一般类没区别
abstract class Animal {
  move(): void {
      console.log('roaming the earch...');
  }
}

抽象类主要是用来被继承使用,抽象方法必须在派生类中必须被实现:

// Animal是抽象类
abstract class Animal {
  // makeSound是抽象方法,没有方法体
  abstract makeSound(): void;
  move(): void {
      console.log('roaming the earch...');
  }
}

// 错误,抽象方法makeSound没有实现
// error TS2515: Non-abstract class 'Dog' does not implement inherited abstract member 'makeSound' from class 'Animal'
class Dog extends Animal {
  // 空类
}

// 正确,抽象方法被实现
class Dog extends Animal {
  makeSound(): void{
    // ...
  }
}

构造器类型

官方文档把构造器类型称为类的静态类型,字面意思比较难理解

构造器类型用来表示具有相同构造器的类,主要是用来描述类的构造者。可以简单的理解为构造器类型的值就是一个类,而本章开篇所讲的类类型的值是一个对象

语法:

new (p1: T1, p2: T2, ...) => T

可以看到,构造器类型的语法和函数类型极为相似,区别是在最前面多了一个 new 关键字:

class TypeA {
  constructor(name: string) {
    // ...
  }
}

// 变量b为构造器类型,和类TypeA的构造器兼容
let b: new (name: string) => TypeA;
b = TypeA;
// b现在是一个类
new b('type');

上面的例子实际上相当于为类 TypeA 定义了一个别名 b。下面再看一个例子:

class TypeA {
  constructor(name: string) {
    // ...
  }
}

// TypeB 类多一个方法 show
class TypeB {
  constructor(name: string) {
    // ...
  }
  show(){
    // ...
  }
}

let c: new (name: string) => TypeA;
// 正确,TypeB除了构造器和TypeA兼容,也兼容TypeA的所有成员
c = TypeB;

let d: new (name: string) => TypeB;
// 错误,TypeA缺少方法show,不能直接赋值给d
// error TS2322: Type 'typeof TypeA' is not assignable to type 'new (name: string) => TypeB'
d = TypeA;

构造器类型在日常开发中使用较少,由于概念比较晦涩,建议只要能看懂语法就行,无须深入

二、泛型

1. 泛型语法

泛型可以为函数、接口、类定义类型变量,这些类型变量就像真实存在的类型一样,可以用于注解值的类型,它的语法形式为:

名字<T1, T2, ...>

名字一般表示函数名、接口名、类名T1, T2, ... 表示一个或多个名字任意类型变量,实际开发中常常以首字母大写的标识符作为类型变量名。泛型在使用时必须以真实类型替换类型变量

(1)泛型函数

// 定义泛型函数,类型变量为T
// T接下来在"参数、返回值、变量"定义中可以作为类型使用
function identity<T>(m: T): T {
  // T 注解了函数内部的变量定义
  let n: T = m;
  return n;
}

// 调用泛型函数,此时用string类型替换类型变量 T
// identity<string> 作为一个整体相当于一个函数名
let m: string = identity<string>('hello world');

(2) 泛型类

// 定义泛型类,包含两个类型变量
class Identity<T1, T2> {
  attr1: T1;
  attr2: T2;
  show(m: T1, n: T2): T2{
    return n;
  }
}

// 用真实类型替换泛型类的类型变量
// Identity<string, number>作为一个整体相当于一个类名
let a: Identity<string, number>;
// 初始化变量a
a = new Identity<string, number>();
// T1=>string,T1被替换为string,属性attr1为字符串类型
a.attr1 = 'hello';
// T2=>number,T2被替换为number,属性attr2位数字类型
a.attr2 = 99;

// 错误,类型不匹配
// error TS2322: Type '"good"' is not assignable to type 'number'
a.attr2 = 'good';

(3)泛型接口

// 定义泛型接口
interface Identity<T> {
  attr: T;
}

// 用真实类型替换泛型接口的类型变量
// Identity<number>作为一个整体相当于一个接口名
let a: Identity<number> = {attr: 10};
// Identity<string>作为一个整体相当于一个接口名
let b: Identity<string> = {attr: 'hello'};

// 错误,类型不匹配,数字10是数字类型,而类型变量为布尔类型
// error TS2322: Type 'number' is not assignable to type 'boolean'.
let c: Identity<boolean> = {attr: 10};

// 一个复杂点的例子
function fn(){};
let c: Identity<typeof fn> = {
  attr(){}
}

2. 泛型约束

必须谨慎使用类型变量,参考下面的例子:

// 定义泛型函数
function getLength<T>(arg: T): number {
  // 错误,编译器不知道类型变量T是否包含属性length,默认为不存在
  // error TS2339: Property 'length' does not exist on type 'T'
  return arg.length;
}

上例中没有任何地方表明 T 类型的参数 arg 包含属性 length,因而编译器认为不存在属性 length 报错。为了改正上述代码,需要约束类型变量 T 必须包含属性 length

(1)语法

类型变量 extends 类型

类型约束关键字为 extends,和继承关键字一样。实际上,类型约束跟继承同义,类型变量继承了被约束类型的所有成员

// 声明接口
interface WithLength {
  length: number;
}

// 正确,T现在被接口类型WithLength约束,包含属性 length
function getLength<T extends WithLength>(arg: T): number {
  return arg.length;
}

(2)默认类型

参考下面的例子:

interface MyType<T> {
  value: T;
}

// error TS2314: Generic type 'MyType<T>' requires 1 type argument(s)
let a: MyType = {
  value: "hello world"
}

泛型在使用的时候必须用实际类型取代类型参数,假如同上面那样,编译器就无法判定a.value的实际类型是什么(有可能是任何类型),因而报错。修正这个错误很简单,只需要在使用的时候传递类型参数即可:

// 正确
let a: MyType<string> = {
  value: "hello world"
}

每次都用尖括号语法注解确实挺烦的,尤其是在有些情况下,其他人使用 MyType 接口的时候可能并不知道这是一个泛型接口声明。TypeScript提供了默认类型语法:

类型变量 = 类型

参考下面的例子:

interface MyType<T = string> {
  value: T;
}

// 正确,在类型参数没有显示指定的情况下,采用了默认类型 string
let x1: MyType = {
  value: "hello world"
}
// 等价于
let x1: MyType<string> = {
  value: "hello world"
}

// 错误, error TS2322: Type 'number' is not assignable to type 'string'
let x2: MyType = {
  value: 123
}

3. 泛型数组

Typescript对数组类型进行了扩展,可以用泛型语法声明数组:

Array<T>

// 只读数组
ReadonlyArray<T>

泛型数组语法比较简单,参考下面的例子:

// 定义数字数组
let arr: number[] = [1, 2, 3];

// 完全等价于
let arr: Array<number> = [1, 2, 3];

只读数组的值一旦初始化,就不能再改变

let arr: ReadonlyArray<number> = [1, 2, 3];

// 错误,只读数组的元素值不能改变
// error TS2542: Index signature in type 'ReadonlyArray<number>' only permits reading
arr[0] = 11;

三、类型转换

1. 类型别名

别名不会创建一个新的类型,它只是原类型的一个引用,和原类型完全等价,它的语法形式如下:

type 别名 = 类型 ;

类型别名可以简化程序,提高可读性和可维护性。以下都是合法的类型别名声明:

// 数字类型别名
type myNumber = number;
// 布尔类型别名
type myBoolean = boolean;
// 联合类型别名
type transition = 'EASE' | 'EASEIN' | 'EASEOUT';
// 联合类型别名
type StringOrNumber = string | number; 
// 联合类型别名
type Text = string | { text: string };  
// 泛型的实际类型别名
type NameLookup = Dictionary<string, Person>;  
// 通过类型查询定义别名
type ObjectStatics = typeof Object;  
// 泛型函数别名
type Callback<T> = (data: T) => void;  
// 元组泛型别名
type Pair<T> = [T, T];  
// 泛型的实际类型别名
type Coordinates = Pair<number>;  
// 联合类型别名
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };

声明了别名以后,别名就相当于是一个类型的标识符,可以用于注解语法中:

// 声明transition为联合类型的别名
type transition = 'EASE' | 'EASEIN' | 'EASEOUT';
// transition此时是一个类型标识符
const boxTransition: transition = 'EASE';

2. 类型断言

类型断言用来明确告诉编译器一个值的类型,相当于类型转换,断言有两种语法格式:

// 1、尖括号语法
<类型表达式>值

// 2、as语法
值 as 类型表达式

为了避免和 JSX 语法产生冲突,尖括号语法只能在 tsx 文件中使用

let someValue: any = "this is a string";

// 1、尖括号语法
let strLength: number = (<string>someValue).length;
// 2、as语法
let strLength: number = (someValue as string).length;

通过一个典型案例演示类型转换的场景:

class Shape {}

class Circle extends Shape {
  showColor() {
    return "red";
  }
}

// Circle是Shape的子类型,因而可以直接作为Shape类型返回
function createShape(kind: string): Shape {
  return new Circle();
}

// circle 被编译器推导为 Shape 类型
var circle = createShape("circle");
// 错误,error TS2339: Property 'showColor' does not exist on type 'Shape'
console.log(circle.showColor());

解决这个问题很简单,只需要对 circle 变量添加类型断言即可,相当于强制类型转换:

// 正确
console.log((<Circle>circle).showColor());

尖括号 <> 的运算符优先级低于点号 . ,因而必须用括号将断言表达式扩起来,直接这样写是错误的:

// 错误,error TS2339: Property 'showColor' does not exist on type 'Shape'
console.log(<Circle>circle.showColor());

在这种情形下,<Circle> 实际上是断言 circle.showColor() 表达式的结果为 Circle 类型

四、模块

1. CommonJS兼容模块

在 Nodejs(CommonJS)中导出模块,只需要将导出对象赋值给 module.exports 即可,而 TypeScript 的模块系统采用的是ES6的 export 语法,两者并不兼容。为了支持 CommonJS 的模块系统,TypeScript增加了支持语法:

// CommonJS 导出
export = 模块导出

// CommonJS 导入
import 名字 = require(模块)

当你在 TypeScript 代码中采用 export =  导出语法时,编译选项 module 必须设置为 commonjs 或 amd

let myobj = {
  a: 1, b: 2
};
export = myobj;

export = 相当于默认导出 export default,因而该语法在一个模块中只能出现一次:

let myobj = {
  a: 1, b: 2
};
export = myobj;

// 错误,默认导出只能存在一个
// error TS2300: Duplicate identifier 'export='
export = {};

若使用 export = 导出一个模块,则必须使用 import 名字 = require(模块) 来导入此模块:

// a.ts
export = {a: 1};

// b.ts
import b = require('./a');

五、命名空间

1. 命名空间

语法

namespace 名字 {
  // ...
}

命名空间引入了新的作用域,大括号可以包含任意合法的代码。要在命名空间之外访问命名空间之内的成员,必须使用 export 关键字:

// 定义命名空间 ns
namespace ns {
  export let a = 'hello world';
  let b = 1;

  // 正确,可以访问b,因为函数show和变量b在同一个命名空间之内
  function show(){
    console.log(b);
  }
}

// 正确,a被export,可以在ns之外访问
let c: string = ns.a;

// 错误,b不允许在ns之外访问
// error TS2339: Property 'b' does not exist on type 'typeof ns'
let d: number = ns.b;

对命名空间成员的访问,类似对象成员的访问,都是用点号运算符 .

2. 空间拆分

命名空间可以拆分,当命名空间代码量很大时,拆分可以提高可维护性:

// 拆分成第一块
namespace ns {
  let a = 1;
}
// 拆分成第二块
namespace ns {
  let b = 2;
}

但要注意,虽然 ns 被拆分成两个代码块了,但这两个代码块之间的成员无法互相访问

namespace ns {
  let a = 1;
}
namespace ns {
  // 错误,error TS2339: Property 'a' does not exist on type 'typeof ns'
  let b = a + 1;
}

要想访问被拆分的命名空间成员,必须用 export 导出需要被其他块访问的成员:

namespace ns {
  export let a = 1;
}
namespace ns {
  // 正确,a被export
  let b = a + 1;
}

命名空间也可以被拆分到不同的文件中

3. 空间嵌套

命名空间可以嵌套,嵌套的层数不受限制:

namespace A {
  namespace B {
    namespace C {
      // ...
    }
  }
}

嵌套的命名空间遵循普通成员一样的规则,要在外面访问被嵌套的命名空间,必须使用关键字 export:

namespace A {
  namespace B {
    export let msg = 'hello world';
  }
}

// 错误,子命名空间B没有被export
// error TS2339: Property 'B' does not exist on type 'typeof A'
console.log(A.B.msg);

要改正这个错误,只需要在命名空间前加修饰符 export

namespace A {
  export namespace B {
    export let msg = 'hello world';
  }
}

4. 空间别名

可以为命名空间或命名空间成员设置别名,这对于访问嵌套过深的命名空间成员的代码简化特别有用。别名关键字为 import:

namespace A {
  export namespace B {
    export namespace C {
      export let msg = 'hello world';
    }
  }
}

// import为子命名空间C定义别名
import N = A.B.C;
// 输出: hello world
console.log(N.msg);

//import为子命名空间C的成员msg定义别名
import m = A.B.C.msg;
//输出:hello world
console.log(m);

实际上,在当前的编译器版本中,import关键字完全可以替换为letconst,下面的代码和上面的等价:

namespace A {
  export namespace B {
    export namespace C {
      export let msg = 'hello world';
    }
  }
}

// let为子命名空间C定义别名
let N = A.B.C;
// 输出: hello world
console.log(N.msg);

//const为子命名空间C的成员msg定义别名
const m = A.B.C.msg;
//输出:hello world
console.log(m);

六、理解声明

1. 为什么需要声明

声明的本质是告知编译器一个标识符的类型信息

声明在TypeScript中至关重要,只有通过声明才能告知编译器一个随机出现的标识符到底代表什么含义。对于语言关键字之外的任意标识符,如果编译器无法获取它的声明,将会报错:

// 错误,凭空出现的variable, 编译器无法知道它代表什么含义
// error TS2304: Cannot find name 'variable'
console.log(variable);

改正这个错误,需要添加 variable 的声明信息:

// 声明语句
let variable: number;
// 正确,已声明variable为数字
console.log(variable);

虽然编译成JavaScript代码执行时仍然会报错,但因为添加了variable的声明信息,在TypeScript中逻辑合理,不会报错

2. 内部声明

到目前为止,所有在TypeScript源码中出现的声明,都是内部声明:

// 声明a为一个数字
let a: number;
// 声明b为一个数字并初始化为2
let b: number = 2;

// 声明T为一个接口
interface T {}
// 声明接口类型变量b
let b: T;

// 声明fn为一个函数
function fn(){}

// 声明myFunc为一个函数
// 此处利用了类型推导
let myFunc = function(a: number){}

// 声明MyEnum枚举类型
enum MyEnum {
  A, B
}

// 声明NS为命名空间
namespace NS {}

// ...

内部声明主要是你当前所写的代码中的所有变量和类型的声明

3. 外部声明

外部声明一般针对第三方来历不明的库,当你想要在你的typescript项目中使用用javascript代码写的第三方库时,就需要用到外部声明。一个常见的例子,假设我们在HTML中通过script标签引入了全局jQuery:

// 注册全局变量 $
<script src="path/to/jquery.js"></script>

path/to/jquery.js 文件在会在全局作用域中引入对象 $,接下来如果在同一项目下的TypeScript文件中使用 $,TypeScript编译器会报错:

// 错误,缺少名字 $ 的声明信息
// error TS2581: Cannot find name '$'. Do you need to install type definitions for jQuery? Try `npm i @types/jquery`
$('body').html('hello world');

由于没有任何类型信息,TypeScript编译器根本不知道 $ 代表的是什么,此时需要引入外部声明(因为$是外部JavaScript引入TypeScript代码中的)。外部声明的关键字是:

declare 

分析语句 $('body').html('hello world'); 得出:

  • $是一个函数,接收字符串参数
  • $调用返回值是一个对象,此对象拥有成员函数 html,这个成员函数的参数也是字符串类型
// 声明 $ 的类型信息
declare let $: (selector: string) => {
  html: (content: string) => void;
};

// 正确,$已经通过外部声明
$('body').html('hello world');

声明应该是纯粹对于一个标识符类型或外观的描述,便于编译器识别,外部声明具有以下特点:

  • 必须使用 declare 修饰外部声明
  • 不能包含实现或初始化信息(内部声明可以在声明的时候包含实现或初始化)
// 声明a为一个数字
declare let a: number;
// 错误,外部声明不能初始化
// error TS1039: Initializers are not allowed in ambient contexts
declare let b: number = 2;

// 声明T为一个接口
declare interface T {}
// 声明接口类型变量b
let b: T;

// 声明fn为一个函数
// 错误,声明包含了函数实现
// error TS1183: An implementation cannot be declared in ambient contexts
declare function fn(){}

// 正确,不包含函数体实现
declare function fn(): void;

// 声明myFunc为一个函数
declare let myFunc: (a: number) => void;

// 声明MyEnum枚举类型
declare enum MyEnum {
  A, B
}

// 声明NS为命名空间
declare namespace NS {
  // 错误,声明不能初始化
  // error TS1039: Initializers are not allowed in ambient contexts
  const a: number = 1;
  // 正确,仅包含声明
  const b: number;
  // 正确,函数未包含函数体实现
  function c(): void;
}

// 声明一个类
declare class Greeter {
    constructor(greeting: string);
    greeting: string;
    showGreeting(): void;
}

外部声明还可以用于声明一个模块,如果一个外部模块的成员要被外部访问,模块成员应该用 export 声明导出:

declare module 'io' {
  export function read(file: string): string;
  export function write(file: string, data: string): void;
}

4. 三斜线指令和d.ts文件

习惯上,常常把外部声明写在一个后缀名为 .d.ts 的声明文件中,然后用三斜线指令引入进来

// jquery.d.ts 文件
declare let $: (selector: string) => {
  html: (content: string) => void;
};

// main.ts 文件
/// <reference path="./jquery.d.ts" />
$('body').html('hello world');

上述语句声明了 main.ts 依赖 jquery.d.ts 声明文件,在编译阶段,被依赖文件 jquery.d.ts 将被包含进来,就像将被依赖文件的源码展开在依赖声明处一样:

// main.ts文件等价于将代码在三斜线指令处展开
declare let $: (selector: string) => {
  html: (content: string) => void;
};
$('body').html('hello world');

三斜线指令中需要注意的是 path 类型和 types 类型的区别:

/// <reference path="./jquery.d.ts" />
/// <reference types="node" />
  • path 类型声明的是对本地文件的依赖,包含路径信息
  • types 类型声明的是对 node_modules/@types 文件夹下的类型的依赖,不包含路径信息
系列目录<< Obsidian插件Dataview —— DQL查询语言详解(五)Obsidian插件Dataview —— 深入理解DataviewJS(七) >>