TypeScript基础入门之模块

264 阅读6分钟

TypeScript基础入门之模块解析(一)

模块解析

本节假设有关模块的一些基本知识。有关更多信息,请参阅模块文档。

模块解析是编译器用来确定导入所引用内容的过程。
考虑一个导入语句,如

import { a } from "moduleA";

为了检查a的任何使用,编译器需要确切地知道它代表什么,并且需要检查它的定义moduleA。

此时,编译器将询问"moduleA的类型是什么?“虽然这听起来很简单,但是moduleA可以在您自己的.ts/.tsx文件中定义,或者在您的代码所依赖的.d.ts中定义。

首先,编译器将尝试查找表示导入模块的文件。
为此,编译器遵循两种不同策略之一:Classic或Node。
这些策略告诉编译器在哪里寻找moduleA。

如果这不起作用并且模块名称是非相对的(并且在"moduleA"的情况下,则是),则编译器将尝试查找环境模块声明。
接下来我们将介绍非相对进口。

最后,如果编译器无法解析模块,它将记录错误。
在这种情况下,错误就像

error TS2307: Cannot find module 'moduleA'

相对与非相对模块导入

根据模块引用是相对引用还是非相对引用,模块导入的解析方式不同。

相对导入是以/、./或../开头的导入。
一些例子包括:

  • import Entry from "./components/Entry";
  • import { DefaultHeaders } from "../constants/http";
  • import "/mod";

任何其他import都被视为非亲属。
一些例子包括:

  • import * as $ from "jquery";
  • import { Component } from "@angular/core";

相对导入是相对于导入文件解析的,无法解析为环境模块声明。
您应该为自己的模块使用相对导入,这些模块可以保证在运行时保持其相对位置。

可以相对于baseUrl或通过路径映射解析非相对导入,我们将在下面介绍。
他们还可以解析为环境模块声明。
导入任何外部依赖项时,请使用非相对路径。

模块解决策略

有两种可能的模块解析策略:Node和Classic。
您可以使用--moduleResolution标志指定模块解析策略。
如果未指定,则默认为Classic for

--module AMD | System | ES2015
或其他Node。

Classic 策略

这曾经是TypeScript的默认解析策略。
如今,这种策略主要用于向后兼容。

将相对于导入文件解析相对导入。
因此,从源文件/root/src/folder/A.ts中的

import { b } from "./moduleB"
将导致以下查找:

  • /root/src/folder/moduleB.ts
  • /root/src/folder/moduleB.d.ts

但是,对于非相对模块导入,编译器会从包含导入文件的目录开始遍历目录树,尝试查找匹配的定义文件。

例如:

在源文件/root/src/folder/A.ts中对moduleB进行非相对导入(例如

import { b } from "moduleB")
将导致尝试以下位置来定位"moduleB":

  • /root/src/folder/moduleB.ts
  • /root/src/folder/moduleB.d.ts
  • /root/src/moduleB.ts
  • /root/src/moduleB.d.ts
  • /root/moduleB.ts
  • /root/moduleB.d.ts
  • /moduleB.ts
  • /moduleB.d.ts

Node 策略

他的解析策略试图在运行时模仿Node.js模块解析机制。Node.js模块文档中概述了完整的Node.js解析算法。

Node.js如何解析模块

要了解TS编译器将遵循的步骤,重要的是要阐明Node.js模块。
传统上,Node.js中的导入是通过调用名为require的函数来执行的。
Node.js采取的行为将根据require是否给定相对路径或非相对路径而有所不同。

相对路径相当简单。
例如,让我们考虑位于

/root/src/moduleA.js
的文件,其中包含import
var x = require("./ moduleB");

Node.js按以下顺序解析导入:

1. 询问名为

/root/src/moduleB.js
的文件(如果存在)。
2. 询问文件夹
/root/src/moduleB
是否包含名为package.json的文件,该文件指定了"main"模块。在我们的示例中,如果Node.js找到包含
{"main": "lib/mainModule.js"}
的文件
/root/src/moduleB/package.json
,那么Node.js将引用
/root/src/moduleB/lib/mainModule.js

3. 询问文件夹/root/src/moduleB是否包含名为index.js的文件。该文件被隐含地视为该文件夹的"main"模块。

您可以在Node.js文档中了解有关文件模块和文件夹模块的更多信息。

但是,非相对模块名称的解析以不同方式执行。
Node将在名为node_modules的特殊文件夹中查找模块。
node_modules文件夹可以与当前文件位于同一级别,或者在目录链中位于更高级别。
Node将走向目录链,查看每个node_modules,直到找到您尝试加载的模块。

按照上面的示例,考虑是否

/root/src/moduleA.js
使用非相对路径并且导入
var x = require("moduleB");
然后,Node会尝试将moduleB解析到每个位置,直到一个工作。

  • /root/src/node_modules/moduleB.js
  • /root/src/node_modules/moduleB/package.json (if it specifies a "main" property)
  • /root/src/node_modules/moduleB/index.js
  • /root/node_modules/moduleB.js
  • /root/node_modules/moduleB/package.json (if it specifies a "main" property)
  • /root/node_modules/moduleB/index.js
  • /node_modules/moduleB.js
  • /node_modules/moduleB/package.json (if it specifies a "main" property)
  • /node_modules/moduleB/index.js

请注意,Node.js在步骤(4)和(7)中跳过了一个目录。

您可以在Node.js文档中阅读有关从node_modules加载模块的过程的更多信息。

TypeScript如何解析模块

TypeScript将模仿Node.js运行时解析策略,以便在编译时定位模块的定义文件。
为此,TypeScript通过Node的解析逻辑覆盖TypeScript源文件扩展名(.ts、.tsx和.d.ts)。
TypeScript还将使用package.json中名为"types"的字段来镜像"main"的目的 - 编译器将使用它来查找要查询的"main"定义文件。

例如,

/root/src/moduleA.ts
中的
import { b } from "./moduleB"
等导入语句将导致尝试以下位置来定位"./moduleB":

  • /root/src/moduleB.ts
  • /root/src/moduleB.tsx
  • /root/src/moduleB.d.ts
  • /root/src/moduleB/package.json (if it specifies a "types" property)
  • /root/src/moduleB/index.ts
  • /root/src/moduleB/index.tsx
  • /root/src/moduleB/index.d.ts

回想一下,Node.js查找名为moduleB.js的文件,然后查找适用的package.json,然后查找index.js。

类似地,非相对导入将遵循Node.js解析逻辑,首先查找文件,然后查找适用的文件夹。
因此,从源文件

/root/src/moduleA.ts
中的
import { b } from "moduleB"
将导致以下查找:

  • /root/src/node_modules/moduleB.ts
  • /root/src/node_modules/moduleB.tsx
  • /root/src/node_modules/moduleB.d.ts
  • /root/src/node_modules/moduleB/package.json (if it specifies a "types" property)
  • /root/src/node_modules/moduleB/index.ts
  • /root/src/node_modules/moduleB/index.tsx
  • /root/src/node_modules/moduleB/index.d.ts
  • /root/node_modules/moduleB.ts
  • /root/node_modules/moduleB.tsx
  • /root/node_modules/moduleB.d.ts
  • /root/node_modules/moduleB/package.json (if it specifies a "types" property)
  • /root/node_modules/moduleB/index.ts
  • /root/node_modules/moduleB/index.tsx
  • /root/node_modules/moduleB/index.d.ts
  • /node_modules/moduleB.ts
  • /node_modules/moduleB.tsx
  • /node_modules/moduleB.d.ts
  • /node_modules/moduleB/package.json (if it specifies a "types" property)
  • /node_modules/moduleB/index.ts
  • /node_modules/moduleB/index.tsx
  • /node_modules/moduleB/index.d.ts

不要被这里的步骤数吓倒 - TypeScript仍然只在步骤(8)和(15)两次跳过目录。
这实际上并不比Node.js本身正在做的复杂。

附加模块分辨率标志

项目源布局有时与输出的布局不匹配。
通常,一组构建步骤会导致生成最终输出。
这些包括将.ts文件编译为.js,以及将不同源位置的依赖项复制到单个输出位置。
最终结果是运行时的模块可能具有与包含其定义的源文件不同的名称。
或者,在编译时,最终输出中的模块路径可能与其对应的源文件路径不匹配。

TypeScript编译器具有一组附加标志,用于通知编译器预期发生在源上的转换以生成最终输出。

重要的是要注意编译器不会执行任何这些转换;
它只是使用这些信息来指导将模块导入解析到其定义文件的过程。

TypeScript基础入门之模块解析(二)

继续上文[TypeScript基础入门之模块解析(一)]

模块解析

Base URL

使用baseUrl是使用AMD模块加载器的应用程序中的常见做法,其中模块在运行时"deployed"到单个文件夹。
这些模块的源代码可以存在于不同的目录中,但构建脚本会将它们放在一起。

设置baseUrl通知编译器在哪里可以找到模块。
假定所有具有非相对名称的模块导入都相对于baseUrl

baseUrl的值确定为:

1) baseUrl命令行参数的值(如果给定的路径是相对的,则根据当前目录计算)
2) 'tsconfig.json'中baseUrl属性的值(如果给定的路径是相对的,则根据'tsconfig.json'的位置计算)

请注意,设置baseUrl不会影响相对模块导入,因为它们始终相对于导入文件进行解析。
您可以在RequireJS和SystemJS文档中找到有关baseUrl的更多文档。

路径映射(Path mapping)

有时模块不直接位于baseUrl下。
例如,对模块"jquery"的导入将在运行时转换为"node_modules/jquery/dist/jquery.slim.min.js"。
加载程序使用映射配置在运行时将模块名称映射到文件,请参阅RequireJs文档和SystemJS文档

TypeScript编译器支持使用tsconfig.json文件中的"paths"属性声明此类映射。
以下是如何为jquery指定"paths"属性的示例。

{
  "compilerOptions": {
    "baseUrl": ".", // This must be specified if "paths" is.
    "paths": {
      "jquery": ["node_modules/jquery/dist/jquery"] // This mapping is relative to "baseUrl"
    }
  }
}

请注意,相对于"baseUrl"解析"paths"。
当将"baseUrl"设置为"."之外的另一个值,即tsconfig.json的目录时,必须相应地更改映射。
比如说,你在上面的例子中设置了"baseUrl": "./src",然后jquery应该映射到"../node_modules/jquery/dist/jquery"

使用"paths"还允许更复杂的映射,包括多个后退位置。
考虑一个项目配置,其中只有一些模块在一个位置可用,其余模块在另一个位置。
构建步骤将它们放在一个地方。
项目布局可能如下所示:

projectRoot
├── folder1
│   ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│   └── file2.ts
├── generated
│   ├── folder1
│   └── folder2
│       └── file3.ts
└── tsconfig.json


相应的tsconfig.json如下所示:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "*": [
        "*",
        "generated/*"
      ]
    }
  }
}

这告诉编译器任何与模式"*"(即所有值)匹配的模块导入,以查看两个位置:

1) "*"表示同名不变,因此map

<moduleName> => <baseUrl>/<moduleName>

2) "generated/*"表示带有附加前缀"generated"的模块名称,因此map
<moduleName> => <baseUrl>/generated/<moduleName>

遵循此逻辑,编译器将尝试解析这两个导入:

1) import 'folder1/file2'
1. 模式'*'匹配,通配符捕获整个模块名称
2. 尝试列表中的第一个替换:'*' -> folder1/file2
3. 替换结果是非相对名称 - 将其与

baseUrl -> projectRoot/folder1/file2.ts
结合使用。
4. 文件已存在。完成。

2) import 'folder2/file3'
1. 模式'*'匹配,通配符捕获整个模块名称
2. 尝试列表中的第一个替换:'*' -> folder2/file3
3. 替换结果是非相对名称 - 将其与

baseUrl -> projectRoot/folder2/file3.ts
结合使用。
4. 文件不存在,移动到第二个替换
5. 第二次替换'generated/*' -> generated/folder2/file3
6. 替换结果是非相对名称 - 将它与baseUrl -> projectRoot/generated/folder2/file3.ts结合使用
7. 文件已存在。完成。

使用rootDirs的虚拟目录

有时,编译时来自多个目录的项目源都被组合在一起以生成单个输出目录。
这可以看作是一组源目录创建一个"虚拟"目录。

使用'rootDirs',您可以通知编译器构成此"虚拟"目录的根;
因此编译器可以解析这些"虚拟"目录中的相关模块导入,就像在一个目录中合并在一起一样。

例如,考虑这个项目结构:

src
└── views
     └── view1.ts (imports './template1')
     └── view2.ts

generated
└── templates
    └── views
        └── template1.ts (imports './view2')

src/views中的文件是某些UI控件的用户代码。
生成/模板中的文件是由模板生成器自动生成的UI模板绑定代码,作为构建的一部分。
构建步骤会将/src/views和/generated/templates/views中的文件复制到输出中的同一目录。
在运行时,视图可以期望其模板存在于其旁边,因此应使用相对名称"./template"将其导入。

要指定与编译器的此关系,请使用"rootDirs"。
"rootDirs"指定一个根列表,其内容应在运行时合并。
因此,按照我们的示例,tsconfig.json文件应如下所示:

{
  "compilerOptions": {
    "rootDirs": [
      "src/views",
      "generated/templates/views"
    ]
  }
}

每次编译器在其中一个rootDirs的子文件夹中看到相对模块导入时,它将尝试在rootDirs的每个条目中查找此导入。

rootDirs的灵活性不仅限于指定逻辑合并的物理源目录列表。
所提供的阵列可以包括任意数量的ad hoc,任意目录名,而不管它们是否存在。
这允许编译器以类型安全的方式捕获复杂的捆绑和运行时功能,例如条件包含和项目特定的加载器插件。

考虑一种国际化场景,其中构建工具通过插入特殊路径令牌(例如#{locale})自动生成特定于语言环境的包,作为相对模块路径的一部分,例如./#{locale}/messages。
在此假设设置中,该工具枚举支持的语言环境,将抽象的路径映射到./zh/messages,./de/messages等。

假设每个模块都导出一个字符串数组。
例如./zh/messages可能包含:

export default [
    "您好吗",
    "很高兴认识你"
];

通过利用rootDirs,我们可以通知编译器这个映射,从而允许它安全地解析./#{locale}/messages,即使该目录永远不存在。
例如,使用以下tsconfig.json配置:

{
  "compilerOptions": {
    "rootDirs": [
      "src/zh",
      "src/de",
      "src/#{locale}"
    ]
  }
}

编译器现在将解析来自'./#{locale}/messages'的导入消息,以便从工具中导入来自'./zh/messages'的消息,允许以区域设置无关的方式进行开发,而不会影响设计时支持。

TypeScript基础入门之模块解析(三)

继续上文[TypeScript基础入门之模块解析(二)]

跟踪模块解析

如前所述,编译器可以在解析模块时访问当前文件夹之外的文件。
在诊断模块未解析的原因或解析为错误定义时,这可能很难。
使用--traceResolution启用编译器模块分辨率跟踪可以深入了解模块解析过程中发生的情况。

假设我们有一个使用typescript模块的示例应用程序。
app.ts有一个导入,比如import * as ts from "typescript"。

│   tsconfig.json
├───node_modules
│   └───typescript
│       └───lib
│               typescript.d.ts
└───src
    └───app.ts

使用--traceResolution调用编译器

tsc --traceResolution

输出结果如下:

======== Resolving module 'typescript' from 'src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'typescript' from 'node_modules' folder.
File 'src/node_modules/typescript.ts' does not exist.
File 'src/node_modules/typescript.tsx' does not exist.
File 'src/node_modules/typescript.d.ts' does not exist.
File 'src/node_modules/typescript/package.json' does not exist.
File 'node_modules/typescript.ts' does not exist.
File 'node_modules/typescript.tsx' does not exist.
File 'node_modules/typescript.d.ts' does not exist.
Found 'package.json' at 'node_modules/typescript/package.json'.
'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
File 'node_modules/typescript/lib/typescript.d.ts' exist - use it as a module resolution result.
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========

值得关注的事情

1. 导入的名称和位置

======== Resolving module 'typescript' from 'src/app.ts'. ========

2. 编译器遵循的策略

Module resolution kind is not specified, using 'NodeJs'.

3. 从npm包加载类型

'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.

4. 最后结果

======== Module name ‘typescript’ was successfully resolved to ‘node_modules/typescript/lib/typescript.d.ts’. ========

使用--noResolve

通常,编译器将在启动编译过程之前尝试解析所有模块导入。
每次成功解析导入到文件时,该文件都会添加到编译器稍后将处理的文件集中。

--noResolve编译器选项指示编译器不要将任何文件"add"到未在命令行上传递的编译中。
它仍将尝试将模块解析为文件,但如果未指定该文件,则不会包含该文件。

例如:

app.ts

import * as A from "moduleA" // OK, 'moduleA' passed on the command-line
import * as B from "moduleB" // Error TS2307: Cannot find module 'moduleB'.
tsc app.ts moduleA.ts --noResolve

使用--noResolve编译app.ts应该导致:
1. 正确查找在命令行上传递的moduleA。
2. 找不到未通过的moduleB时出错。

常见问题

为什么排除列表中的模块仍然被编译器拾取?

tsconfig.json将文件夹转换为"project"。如果不指定任何"exclude"或"files"条目,则包含tsconfig.json及其所有子目录的文件夹中的所有文件都包含在编译中。
如果要排除某些文件使用"exclude",如果您希望指定所有文件而不是让编译器查找它们,请使用"files"。

那是tsconfig.json自动包含。
这并没有嵌入上面讨论的模块解析。
如果编译器将文件标识为模块导入的目标,则无论它是否在前面的步骤中被排除,它都将包含在编译中。

因此,要从编译中排除文件,您需要将其和所有具有import或

/// <reference path ="..."/>
指令的文件排除在外。

TypeScript基础入门之声明合并(一)

声明合并

介绍

TypeScript中的一些独特概念描述了类型级别的JavaScript对象的形状。
TypeScript特别独特的一个例子是"声明合并"的概念。
在使用现有JavaScript时,理解此概念将为您提供优势。
它还为更高级的抽象概念打开了大门。

出于本文的目的,"声明合并"意味着编译器将使用相同名称声明的两个单独声明合并到单个定义中。
此合并定义具有两个原始声明的功能。
可以合并任意数量的声明;
它不仅限于两个声明。

基本概念

在TypeScript中,声明在三个组中的至少一个中创建实体:名称空间,类型或值。
命名空间创建声明创建一个命名空间,其中包含使用点符号表示法访问的名称。
类型创建声明就是这样做的:它们创建一个可以使用声明的形状显示并绑定到给定名称的类型。
最后,创建值的声明会创建在输出JavaScript中可见的值。

Declaration TypeNamespaceTypeValue
NamespaceXX
ClassXX
EnumXX
InterfaceX
Type AliasX
FunctionX
VariableX

了解每个声明创建的内容将帮助您了解执行声明合并时合并的内容。

合并接口

最简单,也许是最常见的声明合并类型是接口合并。
在最基本的层面上,合并机械地将两个声明的成员连接到具有相同名称的单个接口。

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};

接口的非功能成员应该是唯一的。
如果它们不是唯一的,则它们必须属于同一类型。
如果接口都声明了具有相同名称但具有不同类型的非函数成员,则编译器将发出错误。

对于函数成员,同名的每个函数成员都被视为描述同一函数的重载。
值得注意的是,在接口A与后面的接口A合并的情况下,第二接口将具有比第一接口更高的优先级。

也就是说,在示例中:

interface Cloner {
    clone(animal: Animal): Animal;
}

interface Cloner {
    clone(animal: Sheep): Sheep;
}

interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
}

三个接口将合并以创建单个声明,如下所示:

interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
    clone(animal: Sheep): Sheep;
    clone(animal: Animal): Animal;
}

请注意,每个组的元素保持相同的顺序,但组本身与稍后排序的后续重载集合在一起。

此规则的一个例外是专门签名。
如果签名的参数类型是单个字符串文字类型(例如,不是字符串文字的并集),那么它将被冒泡到其合并的重载列表的顶部。

例如,以下接口将合并在一起:

interface Document {
    createElement(tagName: any): Element;
}
interface Document {
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
    createElement(tagName: string): HTMLElement;
    createElement(tagName: "canvas"): HTMLCanvasElement;
}

由此产生的合并声明文件将如下:

interface Document {
    createElement(tagName: "canvas"): HTMLCanvasElement;
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
    createElement(tagName: string): HTMLElement;
    createElement(tagName: any): Element;
}

TypeScript基础入门之声明合并(二)

声明合并

合并命名空间

与接口类似,同名的命名空间也将合并其成员。
由于名称空间同时创建了名称空间和值,因此我们需要了解它们是如何合并的。

要合并命名空间,每个命名空间中声明的导出接口的类型定义本身已合并,形成一个内部具有合并接口定义的命名空间。

要合并命名空间值,在每个声明站点,如果已存在具有给定名称的命名空间,则通过获取现有命名空间并将第二个命名空间的导出成员添加到第一个命名空间来进一步扩展它。

在此示例中,Animals的声明合并:

namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}

相当于:

namespace Animals {
    export interface Legged { numberOfLegs: number; }

    export class Zebra { }
    export class Dog { }
}

这种命名空间合并模型是一个有用的起点,但我们还需要了解非导出成员会发生什么。
非导出成员仅在原始(未合并)命名空间中可见。
这意味着在合并之后,来自其他声明的合并成员无法看到未导出的成员。

在这个例子中我们可以更清楚地看到这一点:

namespace Animal {
    let haveMuscles = true;

    export function animalsHaveMuscles() {
        return haveMuscles;
    }
}

namespace Animal {
    export function doAnimalsHaveMuscles() {
        return haveMuscles;  // Error, because haveMuscles is not accessible here
    }
}

由于未导出hasMuscles,因此只有共享相同未合并命名空间的animalsHaveMuscles函数才能看到该符号。
doAnimalsHaveMuscles函数,即使它是合并的Animal命名空间的一部分,也无法看到此未导出的成员。

TypeScript基础入门之声明合并(三)

声明合并

将命名空间与类,函数和枚举合并

命名空间足够灵活,也可以与其他类型的声明合并。
为此,命名空间声明必须遵循它将与之合并的声明。
生成的声明具有两种声明类型的属性。
TypeScript使用此功能来模拟JavaScript以及其他编程语言中的某些模式。


将命名空间与类合并

这为用户提供了一种描述内部类的方法。

class Album {
    label: Album.AlbumLabel;
}

namespace Album {
    export class AlbumLabel { }
}

合并成员的可见性规则与"合并命名空间"部分中描述的相同,因此我们必须导出合并类的AlbumLabel类才能看到它。
最终结果是在另一个类内部管理的类。
您还可以使用命名空间向现有类添加更多静态成员。

除了内部类的模式之外,您还可能熟悉创建函数的JavaScript实践,然后通过向函数添加属性来进一步扩展函数。
TypeScript使用声明合并以类型安全的方式构建这样的定义。

function buildLabel(name: string): string {
    return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
    export let suffix = "";
    export let prefix = "Hello, ";
}

console.log(buildLabel("Sam Smith"));

同样,名称空间可用于扩展具有静态成员的枚举:

enum Color {
    red = 1,
    green = 2,
    blue = 4
}

namespace Color {
    export function mixColor(colorName: string) {
        if (colorName == "yellow") {
            return Color.red + Color.green;
        }
        else if (colorName == "white") {
            return Color.red + Color.green + Color.blue;
        }
        else if (colorName == "magenta") {
            return Color.red + Color.blue;
        }
        else if (colorName == "cyan") {
            return Color.green + Color.blue;
        }
    }
}

不允许合并

并非TypeScript中允许所有合并。
目前,类不能与其他类或变量合并。
有关模拟类合并的信息,请参阅TypeScript中的Mixins部分。

模块扩展

虽然JavaScript模块不支持合并,但您可以通过导入然后更新它们来修补现有对象。
让我们看一下玩具Observable示例:

// observable.js
export class Observable<T> {
    // ... implementation left as an exercise for the reader ...
}

// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
    // ... another exercise for the reader
}

这在TypeScript中也可以正常工作,但编译器不了解Observable.prototype.map。
您可以使用模块扩充来告诉编译器:

// observable.ts stays the same
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
    interface Observable<T> {
        map<U>(f: (x: T) => U): Observable<U>;
    }
}
Observable.prototype.map = function (f) {
    // ... another exercise for the reader
}


// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());

模块名称的解析方式与导入/导出中的模块说明符相同。
有关更多信息,请参阅模块
然后合并扩充中的声明,就好像它们在与原始文件相同的文件中声明一样。
但是,您无法在扩充中声明新的顶级声明 - 只是现有声明的补丁。

全局扩展您还可以从模块内部向全局范围添加声明:

// observable.ts
export class Observable<T> {
    // ... still no implementation ...
}

declare global {
    interface Array<T> {
        toObservable(): Observable<T>;
    }
}

Array.prototype.toObservable = function () {
    // ...
}


全局扩展与模块扩展具有相同的行为和限制。

TypeScript基础入门之Javascript文件类型检查(一)

TypeScript 2.3及更高版本支持使用--checkJs在.js文件中进行类型检查和报告错误。

您可以通过添加//@ts-nocheck注释来跳过检查某些文件; 相反,您可以通过在不设置--checkJs的情况下向其添加//@ts-check注释来选择仅检查几个.js文件。 您还可以通过在前一行添加//@ts-ignore来忽略特定行上的错误。 请注意,如果你有一个tsconfig.json,JS检查将遵循严格的标志,如noImplicitAny,strictNullChecks等。但是,由于JS检查相对松散,将严格的标志与它结合可能会令人惊讶。

以下是.js文件中检查与.ts文件相比如何工作的一些显着差异:

JSDoc类型用于类型信息

在.js文件中,通常可以像.ts文件一样推断类型。 同样,当无法推断类型时,可以使用JSDoc指定它们,就像在.ts文件中使用类型注释一样。 就像Typescript一样, --noImplicitAny会给你编译器无法推断类型的地方的错误。 (开放式对象文字除外;有关详细信息,请参见下文。) 装饰声明的JSDoc注释将用于设置该声明的类型。 例如:

/** @type {number} */
var x;

x = 0;      // OK
x = false;  // Error: boolean is not assignable to number

您可以在JavaScript文档中的JSDoc支持中找到受支持的JSDoc模式的完整列表。

属性是从类体中的赋值推断出来的

ES2015没有在类上声明属性的方法。 属性是动态分配的,就像对象文字一样。

在.js文件中,编译器从类主体内的属性赋值中推断属性。 属性的类型是构造函数中给出的类型,除非它没有在那里定义,或者构造函数中的类型是undefined或null。 在这种情况下,类型是这些赋值中所有右侧值的类型的并集。 始终假定构造函数中定义的属性存在,而仅在方法,getter或setter中定义的属性被视为可选。

class C {
    constructor() {
        this.constructorOnly = 0
        this.constructorUnknown = undefined
    }
    method() {
        this.constructorOnly = false // error, constructorOnly is a number
        this.constructorUnknown = "plunkbat" // ok, constructorUnknown is string | undefined
        this.methodOnly = 'ok'  // ok, but y could also be undefined
    }
    method2() {
        this.methodOnly = true  // also, ok, y's type is string | boolean | undefined
    }
}

如果从未在类体中设置属性,则将它们视为未知。 如果您的类具有仅从中读取的属性,请使用JSDoc在构造函数中添加然后注释声明以指定类型。 如果稍后将初始化,您甚至不必提供值:

class C {
    constructor() {
        /** @type {number | undefined} */
        this.prop = undefined;
        /** @type {number | undefined} */
        this.count;
    }
}


let c = new C();
c.prop = 0;          // OK
c.count = "string";  // Error: string is not assignable to number|undefined

构造函数等同于类

在ES2015之前,Javascript使用构造函数而不是类。 编译器支持此模式,并将构造函数理解为与ES2015类等效。 上述属性推理规则的工作方式完全相同。

function C() {
    this.constructorOnly = 0
    this.constructorUnknown = undefined
}
C.prototype.method = function() {
    this.constructorOnly = false // error
    this.constructorUnknown = "plunkbat" // OK, the type is string | undefined
}

支持CommonJS模块

在.js文件中,Typescript了解CommonJS模块格式。 对exports和module.exports的赋值被识别为导出声明。 同样,require函数调用被识别为模块导入。 例如:

// same as `import module "fs"`
const fs = require("fs");

// same as `export function readFile`
module.exports.readFile = function(f) {
    return fs.readFileSync(f);
}

Javascript中的模块支持比Typescript的模块支持更具语法上的宽容。 支持大多数分配和声明组合。

类,函数和对象文字是名称空间 类是.js文件中的命名空间。 这可以用于嵌套类,例如:

class C {
}
C.D = class {
}

并且,对于ES2015之前的代码,它可以用于模拟静态方法:

function Outer() {
  this.y = 2
}
Outer.Inner = function() {
  this.yy = 2
}

它还可以用于创建简单的命名空间:

var ns = {}
ns.C = class {
}
ns.func = function() {
}
// IIFE
var ns = (function (n) {
  return n || {};
})();
ns.CONST = 1

// defaulting to global
var assign = assign || function() {
  // code goes here
}
assign.extra = 1

TypeScript基础入门之Javascript文件类型检查(二)

继续上篇文章【TypeScript基础入门之Javascript文件类型检查(一)

对象文字是开放式的

在.ts文件中,初始化变量声明的对象文字将其类型赋予声明。不能添加未在原始文本中指定的新成员。此规则在.js文件中放宽;对象文字具有开放式类型(索引签名),允许添加和查找最初未定义的属性。例如:

var obj = { a: 1 };
obj.b = 2;  // Allowed

对象文字的行为就像它们具有索引签名[x:string]:任何允许它们被视为开放映射而不是封闭对象的任何东西。

与其他特殊的JS检查行为一样,可以通过为变量指定JSDoc类型来更改此行为。例如:

/** @type {{a: number}} */
var obj = { a: 1 };
obj.b = 2;  // Error, type {a: number} does not have property b

null,undefined和empty数组初始值设定项的类型为any或any[]

使用null或undefined初始化的任何变量,参数或属性都将具有类型any,即使打开了严格的空检查。使用[]初始化的任何变量,参数或属性都将具有类型any[],即使打开了严格的空检查。唯一的例外是具有如上所述的多个初始值设定项的属性。

function Foo(i = null) {
    if (!i) i = 1;
    var j = undefined;
    j = 2;
    this.l = [];
}
var foo = new Foo();
foo.l.push(foo.i);
foo.l.push("end");

功能参数默认是可选的

由于无法在ES2015之前的Javascript中指定参数的可选性,因此.js文件中的所有函数参数都被视为可选参数。允许使用参数少于声明的参数数量的调用。

重要的是要注意,调用具有太多参数的函数是错误的。

例如:

function bar(a, b) {
    console.log(a + " " + b);
}

bar(1);       // OK, second argument considered optional
bar(1, 2);
bar(1, 2, 3); // Error, too many arguments

JSDoc注释函数被排除在此规则之外。使用JSDoc可选参数语法来表示可选性。例如:

/**
 * @param {string} [somebody] - Somebody's name.
 */
function sayHello(somebody) {
    if (!somebody) {
        somebody = 'John Doe';
    }
    console.log('Hello ' + somebody);
}

sayHello();

由arguments推断出的var-args参数声明

其主体具有对参数引用的引用的函数被隐式地认为具有var-arg参数(即(...arg: any[]) => any)。使用JSDoc var-arg语法指定参数的类型。

/** @param {...number} args */
function sum(/* numbers */) {
    var total = 0
    for (var i = 0; i < arguments.length; i++) {
      total += arguments[i]
    }
    return total
}

未指定的类型参数默认为any

由于在Javascript中没有用于指定泛型类型参数的自然语法,因此未指定的类型参数默认为any。

在extends子句中:

例如,React.Component被定义为具有两个类型参数,Props和State。在.js文件中,没有合法的方法在extends子句中指定它们。默认情况下,类型参数将是any:

import { Component } from "react";

class MyComponent extends Component {
    render() {
        this.props.b; // Allowed, since this.props is of type any
    }
}

使用JSDoc @augments明确指定类型。例如:

import { Component } from "react";

/**
 * @augments {Component<{a: number}, State>}
 */
class MyComponent extends Component {
    render() {
        this.props.b; // Error: b does not exist on {a:number}
    }
}

在JSDoc引用中

JSDoc中的未指定类型参数默认为any:

/** @type{Array} */
var x = [];

x.push(1);        // OK
x.push("string"); // OK, x is of type Array<any>


/** @type{Array.<number>} */
var y = [];

y.push(1);        // OK
y.push("string"); // Error, string is not assignable to number

在函数调用中

对泛型函数的调用使用参数来推断类型参数。有时这个过程无法推断任何类型,主要是因为缺乏推理源;在这些情况下,类型参数将默认为any。例如:

var p = new Promise((resolve, reject) => { reject() });

p; // Promise<any>;

TypeScript基础入门之Javascript文件类型检查(三)

继续上篇文章【TypeScript基础入门之Javascript文件类型检查(二)

支持JSDoc

下面的列表概述了使用JSDoc注释在JavaScript文件中提供类型信息时当前支持的构造。

请注意,尚不支持下面未明确列出的任何标记(例如@async)。

含义通常与usejsdoc.org上给出的标记含义相同或超​​集。下面的代码描述了这些差异,并给出了每个标记的一些示例用法。

@type

您可以使用”@type“标记并引用类型名称(原语,在TypeScript声明中定义,或在JSDoc”@typedef“标记中)。您可以使用任何Typescript类型和大多数JSDoc类型。

/**
 * @type {string}
 */
var s;

/** @type {Window} */
var win;

/** @type {PromiseLike<string>} */
var promisedString;

// You can specify an HTML Element with DOM properties
/** @type {HTMLElement} */
var myElement = document.querySelector(selector);
element.dataset.myData = '';

@type可以指定联合类型 - 例如,某些东西可以是字符串或布尔值。

/**
 * @type {(string | boolean)}
 */
var sb;

请注意,括号对于联合类型是可选的。

/**
 * @type {string | boolean}
 */
var sb;

您可以使用各种语法指定数组类型:

/** @type {number[]} */
var ns;
/** @type {Array.<number>} */
var nds;
/** @type {Array<number>} */
var nas;

您还可以指定对象文字类型。例如,具有属性’a’(字符串)和’b’(数字)的对象使用以下语法:

/** @type {{ a: string, b: number }} */
var var9;

您可以使用标准JSDoc语法或Typescript语法,使用字符串和数字索引签名指定类似地图和类似数组的对象。

/**
 * A map-like object that maps arbitrary `string` properties to `number`s.
 *
 * @type {Object.<string, number>}
 */
var stringToNumber;

/** @type {Object.<number, object>} */
var arrayLike;

前两种类型等同于Typescript类型{ [x: string]: number }和{ [x: number]: any }。编译器理解这两种语法。

您可以使用Typescript或Closure语法指定函数类型:

/** @type {function(string, boolean): number} Closure syntax */
var sbn;
/** @type {(s: string, b: boolean) => number} Typescript syntax */
var sbn2;

或者您可以使用未指定的函数类型:

/** @type {Function} */
var fn7;
/** @type {function} */
var fn6;

Closure的其他类型也有效:

/**
 * @type {*} - can be 'any' type
 */
var star;
/**
 * @type {?} - unknown type (same as 'any')
 */
var question;

类型转换

Typescript借用了Closure的强制语法。这允许您通过在任何带括号的表达式之前添加@type标记将类型转换为其他类型。

/**
 * @type {number | string}
 */
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString)

导入类型

您还可以使用导入类型从其他文件导入声明。此语法是特定于Typescript的,与JSDoc标准不同:

/**
 * @param p { import("./a").Pet }
 */
function walk(p) {
    console.log(`Walking ${p.name}...`);
}

导入类型也可以在类型别名声明中使用:

/**
 * @typedef Pet { import("./a").Pet }
 */

/**
 * @type {Pet}
 */
var myPet;
myPet.name;

如果你不知道类型,或者它有一个令人讨厌的大型类型,可以使用import类型从模块中获取值的类型:

/**
 * @type {typeof import("./a").x }
 */
var x = require("./a").x;

TypeScript基础入门之Javascript文件类型检查(四)

继续上篇文章【TypeScript基础入门之Javascript文件类型检查(三)

@param 和 @returns

@param使用与@type相同的类型语法,但添加了参数名称。 通过用方括号括起名称,也可以声明该参数是可选的:

// Parameters may be declared in a variety of syntactic forms
/**
 * @param {string}  p1 - A string param.
 * @param {string=} p2 - An optional param (Closure syntax)
 * @param {string} [p3] - Another optional param (JSDoc syntax).
 * @param {string} [p4="test"] - An optional param with a default value
 * @return {string} This is the result
 */
function stringsStringStrings(p1, p2, p3, p4){
  // TODO
}

同样,对于函数的返回类型:

/**
 * @return {PromiseLike<string>}
 */
function ps(){}

/**
 * @returns {{ a: string, b: number }} - May use '@returns' as well as '@return'
 */
function ab(){}

@typedef, @callback, 和 @param

@typedef可用于定义复杂类型。 类似的语法适用于@param。

/**
 * @typedef {Object} SpecialType - creates a new type named 'SpecialType'
 * @property {string} prop1 - a string property of SpecialType
 * @property {number} prop2 - a number property of SpecialType
 * @property {number=} prop3 - an optional number property of SpecialType
 * @prop {number} [prop4] - an optional number property of SpecialType
 * @prop {number} [prop5=42] - an optional number property of SpecialType with default
 */
/** @type {SpecialType} */
var specialTypeObject;

您可以在第一行使用对象或对象。

/**
 * @typedef {object} SpecialType1 - creates a new type named 'SpecialType'
 * @property {string} prop1 - a string property of SpecialType
 * @property {number} prop2 - a number property of SpecialType
 * @property {number=} prop3 - an optional number property of SpecialType
 */
/** @type {SpecialType1} */
var specialTypeObject1;

@param允许一次性类型规范使用类似的语法。 请注意,嵌套属性名称必须以参数名称为前缀:

/**
 * @param {Object} options - The shape is the same as SpecialType above
 * @param {string} options.prop1
 * @param {number} options.prop2
 * @param {number=} options.prop3
 * @param {number} [options.prop4]
 * @param {number} [options.prop5=42]
 */
function special(options) {
  return (options.prop4 || 1001) + options.prop5;
}

@callback类似于@typedef,但它指定了一个函数类型而不是一个对象类型:

/**
 * @callback Predicate
 * @param {string} data
 * @param {number} [index]
 * @returns {boolean}
 */
/** @type {Predicate} */
const ok = s => !(s.length % 2);

当然,任何这些类型都可以在单行@typedef中使用Typescript语法声明:

/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */

@template

您可以使用@template标记声明泛型类型:

/**
 * @template T
 * @param {T} p1 - A generic parameter that flows through to the return type
 * @return {T}
 */
function id(x){ return x }

使用逗号或多个标签声明多个类型参数:

/**
 * @template T,U,V
 * @template W,X
 */

您还可以在类型参数名称之前指定类型约束。 只限列表中的第一个类型参数:

/**
 * @template {string} K - K must be a string or string literal
 * @template {{ serious(): string }} Seriousalizable - must have a serious method
 * @param {K} key
 * @param {Seriousalizable} object
 */
function seriousalize(key, object) {
  // ????
}

TypeScript基础入门之Javascript文件类型检查(五)

继续上篇文章【TypeScript基础入门之Javascript文件类型检查(四)

@constructor

编译器根据此属性赋值推断构造函数,但如果添加@constructor标记,则可以更好地检查更严格和更好的建议:

/**
 * @constructor
 * @param {number} data
 */
function C(data) {
  this.size = 0;
  this.initialize(data); // Should error, initializer expects a string
}
/**
 * @param {string} s
 */
C.prototype.initialize = function (s) {
  this.size = s.length
}

var c = new C(0);
var result = C(1); // C should only be called with new

使用@constructor,在构造函数C中检查它,因此您将获得初始化方法的建议,如果您传递一个数字,则会出错。 如果您调用C而不是构造它,您也会收到错误。

不幸的是,这意味着也可以调用的构造函数不能使用@constructor。

@this

当编译器有一些上下文可以使用时,它通常可以找出它的类型。 如果没有,您可以使用@this显式指定此类型:

/**
 * @this {HTMLElement}
 * @param {*} e
 */
function callbackForLater(e) {
    this.clientHeight = parseInt(e) // should be fine!
}

@extends

当Javascript类扩展通用基类时,无处可指定类型参数应该是什么。 @extends标记为该类型参数提供了一个位置:

/**
 * @template T
 * @extends {Set<T>}
 */
class SortableSet extends Set {
  // ...
}

请注意,@ extends仅适用于类。 目前,构造函数没有办法扩展一个类。

@enum

@enum标记允许您创建一个对象文字,其成员都是指定的类型。 与Javascript中的大多数对象文字不同,它不允许其他成员。

/** @enum {number} */
const JSDocState = {
  BeginningOfLine: 0,
  SawAsterisk: 1,
  SavingComments: 2,
}

请注意,@enum与Typescript的枚举完全不同,并且简单得多。 但是,与Typescript的枚举不同,@enum可以有任何类型:

/** @enum {function(number): number} */
const Math = {
  add1: n => n + 1,
  id: n => -n,
  sub1: n => n - 1,
}

更多示例

var someObj = {
  /**
   * @param {string} param1 - Docs on property assignments work
   */
  x: function(param1){}
};

/**
 * As do docs on variable assignments
 * @return {Window}
 */
let someFunc = function(){};

/**
 * And class methods
 * @param {string} greeting The greeting to use
 */
Foo.prototype.sayHi = (greeting) => console.log("Hi!");

/**
 * And arrow functions expressions
 * @param {number} x - A multiplier
 */
let myArrow = x => x * x;

/**
 * Which means it works for stateless function components in JSX too
 * @param {{a: string, b: number}} test - Some param
 */
var sfc = (test) => <div>{test.a.charAt(0)}</div>;

/**
 * A parameter can be a class constructor, using Closure syntax.
 *
 * @param {{new(...args: any[]): object}} C - The class to register
 */
function registerClass(C) {}

/**
 * @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
 */
function fn10(p1){}

/**
 * @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
 */
function fn9(p1) {
  return p1.join();
}

已知的模式不受支持

引用值空间中的对象,因为类型不起作用,除非对象也创建类型,如构造函数。

function aNormalFunction() {

}
/**
 * @type {aNormalFunction}
 */
var wrong;
/**
 * Use 'typeof' instead:
 * @type {typeof aNormalFunction}
 */
var right;

对象文字类型中的属性类型的Postfix等于未指定可选属性:

/**
 * @type {{ a: string, b: number= }}
 */
var wrong;
/**
 * Use postfix question on the property name instead:
 * @type {{ a: string, b?: number }}
 */
var right;

如果启用了strictNullChecks,则可空类型仅具有意义:

/**
 * @type {?number}
 * With strictNullChecks: true -- number | null
 * With strictNullChecks: off  -- number
 */
var nullable;

非可空类型没有任何意义,并且被视为原始类型:

/**
 * @type {!number}
 * Just has type number
 */
var normal;

与JSDoc的类型系统不同,Typescript只允许您将类型标记为包含null或不包含null。 没有明确的非可空性 - 如果启用了strictNullChecks,则number不可为空。 如果它关闭,则number可以为空。