TypeScript模块篇

73 阅读10分钟

一、类型的导入与导出

ts 是为 js 提供的额外静态类型,相关代码再编译生成 js 时会被完全删除。

原 ts 文件

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

const p: Point = {x: 0, y: 0};

编译后 js 文件

const p = {x: 0, y: 0};

1. 导入与导出类型

模块中类型的导入与导出添加 type 关键字

export type { Type };

export type { Type } from 'mod';
import type DefaultType from 'mod';

import type { Type } from 'mod';

import type * as TypeNs from 'mod';

注意:导入的类型只能用作类型使用

如:有文件utils.ts

class Point {
  x: number;
  y: number;
}
export type { Point };

其他文件引入 utils.ts 中的类型

import type { Point } from './utils';
const p = new Point();
//        ~~~~~~~~~~~
//        编译错误:'Point' 不能作为值来使用,
//        因为它使用了 ‘import type’导入语句

// ===========================================

// 即便取巧不用 type 也不行
import { Point } from './utils';
const p = new Point();
//        ~~~~~~~~~~~
//        编译错误

其他资料:关于导入与导出的高级用法(链接)。

2. 外部声明

2-1. 外部类型声明

“.d.ts”文件是类型声明文件,“.d” 代表 declaration——声明,编译之后也不会生成对应的“.js”文件。

如果声明文件中不使用 import 或者 export ,那么就是一种全局变量或类型声明,如果想导入其他声明文件而不影响全局性,要使用三斜线指令。

三斜线指令例子:

/// <reference types="sizzle" />
/// <reference path="JQueryStatic.d.ts" />

其中用到了 types 和 path 两种不同的指令。它们的区别是:types 用于声明对另一个库的依赖,而 path 用于声明对另一个文件的依赖。(其他指令用法详情可见官网

2-1-1. 外部变量声明

declare var a: boolean;
declare let b: boolean;
declare const c: boolean;

declare const d; // 默认 any
declare var e: number = 1; // 不允许给变量赋值

2-1-2. 外部函数声明

declare function f(a: string, b: boolean): void;

2-1-3. 外部类声明

declare class C {
  // 静态成员
  public static s0(): string;
  public static s1: string;
  
  // 属性
  public a: number;
  public b: number;
  
  // 构造函数
  constuctor(arg: number);
  
  // 方法
  m(x: number, y: number): number;
  
  // 存取器
  get c(): number;
  set c(value: number);
  
  // 索引签名
  [index: string]: any;
}

2-1-4. 外部枚举声明

declare const enum Foo {
  A,
  B,
}

注意:外部枚举声明(与常规枚举声明不同)

  1. 枚举成员值必须为常量——数字、字符串、简单算术运算
  2. 若省略成员值,会视为计算枚举成员,即不会自增,如 0,1,2 等

2-1-5. 外部命名空间声明

历史产物,namespace 是 ts 早期时为了解决模块化而演进创建的关键字,随着 es6 的出现,由原先的module更名为现在的 namespace。已不推荐使用。

它用来表示变量是一个包含子属性的对象。

declare namespace Foo {
  // 外部变量声明
  export var a: boolean; // 不需要显式添加 export
  let b: boolean;
  const c: boolean;

  // 外部函数声明
  function f(a: string, b: boolean): void;

  // 外部类声明
  class C {
    x: number;
    constructor(x: number);
    y(): void;
  }

  // 接口声明
  interface Point {
    x: number;
    y: number;
  }

  // 枚举声明
  enum E {
    A,
    B,
  }

  // 嵌套的命名空间
  namespace Inner {
    var a: boolean;
  }
}

注意:外部命名空间成员默认为导出成员,没必要显示添加 export(添加也不报错)

const point: Drawing.Point = {x: 0, y:0};

2-1-6. 外部模块声明

declare module 'io' {
  export function readFile(filename: string): string;
}
import { readFile } from 'io';

const content: string = readFile('hello.ts');

2-2. 声明文件使用

2-2-1. 声明文件来源

  • TS 语言内置声明文件
  • 安装的第三方声明文件
  • 自定义的声明文件
a. 内置声明

在TypeScript安装目录下(或者项目node_modules中 typescript)的lib文件夹中

lib.d.ts
lib.dom.d.ts
lib.es2015.d.ts
lib.es2016.d.ts
lib.es2017.d.ts
lib.es2018.d.ts
lib.es2019.d.ts
lib.es2020.d.ts
lib.es5.d.ts
lib.es6.d.ts
···

定义标准的js API,运行环境 API

b. 第三方声明

工程中的第三方代码库。例如:项目根目录/node_modules/antd/lib/index.d.ts

如果依赖库没有ts声明,可以去 Search for typed packages

c. 自定义声明

创建一个“.d.ts”声明文件

例如:自定义一个jquery.d.ts。该声明会将jquery模块的类型设置为any类型

declare module 'jquery';

2-2-2. 模块解析

编译器如何查找并读取导入模块的定义

a. 相对模块导入与非相对模块导入

相对导入:模块名以“/”、“./”和“../”符号开始

// “/” 根目录
import { a } from '/mod';
// “./” 当前目录
import { a } from './mod';
// “../” 上级目录
import { a } from '../mod';

非相对导入:模块名不是以“/”、“./”和“../”符号开始

import { FormInstance } from 'antd';
b. 模块解析策略
  • Classic策略
  • Node策略
1. 相对模块

假设有如下目录

C:\app

`-- a.ts

在 a.ts 中引入相对模块 b

import * as B from './b';

第一阶段,将导入的模块名视为文件b(分别尝试添加 .ts | .tsx | .d.ts 后缀查找)

1)查找文件“C:/app/b.ts”

2)查找文件“C:/app/b.tsx”

3)查找文件“C:/app/b.d.ts”

第二阶段,将导入的模块名视为目录/b/,并在该目录中查找“package.json”,然后解析“package.json”文件中的typings属性和types属性

1)如果“C:/app/b/package.json”文件存在,且包含了typings属性或types属性(假设属性值为“typings.d.ts”)

①查找文件“C:/app/b/typings.d.ts”

②查找文件“C:/app/b/typings.d.ts.ts”(注意,尝试添加“.ts”文件扩展名)

③查找文件“C:/app/b/typings.d.ts.tsx”(注意,尝试添加“.tsx”文件扩展名)

④查找文件“C:/app/b/typings.d.ts.d.ts”(注意,尝试添加“.d.ts”文件扩展名)

⑤如果存在目录“C:/app/b/typings.d.ts/”,那么:

a)查找文件“C:/app/b/typings.d.ts/index.ts”

b)查找文件“C:/app/b/typings.d.ts/index.tsx”

c)查找文件“C:/app/b/typings.d.ts/index.d.ts”

2)查找文件“C:/app/b/index.ts”

3)查找文件“C:/app/b/index.tsx”

4)查找文件“C:/app/b/index.d.ts”

第三阶段,将导入的模块名视为文件,并在指定目录中依次查找JavaScript文件

1)查找文件“C:/app/b.js”。

2)查找文件“C:/app/b.jsx”。

第四阶段,将导入的模块名视为目录,并在该目录中查找“package.json”文件,然后解析“package.json”文件中的main属性

1)如果“C:/app/b/package.json”文件存在,且包含了main属性(假设属性值为“main.js”),那么:

①查找文件“C:/app/b/main.js”。

②查找文件“C:/app/b/main.js.js”(注意,尝试添加“.js”文件扩展名)。

③查找文件“C:/app/b/main.js.jsx”(注意,尝试添加“.jsx”文件扩展名)。

④查找文件“C:/app/b/main.js”(注意,尝试删除文件扩展名后再添加“.js”文件扩展名)。

⑤查找文件“C:/app/b/main.jsx”(注意,尝试删除文件扩展名后再添加“.jsx”文件扩展名)。

⑥如果存在目录“C:/app/b/main.js/”,那么:

a)查找文件“C:/app/b/main.js/index.js”。

b)查找文件“C:/app/b/main.js/index.jsx”。

2)查找文件“C:/app/b/index.js”

3)查找文件“C:/app/b/index.jsx”

流程图:

2. 非相对模块

在 a.ts 中引入非相对模块 b

import * as B from 'b';

流程图:

c.导入外部模块声明

在上述 Node 模块解析策略中,编译器都是在尝试查找一个与导入模块相匹配的文件。

但如果最终未能找到这样的模块文件并且导入语句是非相对模块导入,那么编译器将继续在外部模块声明中查找导入的模块。

有如下目录结构:

C:\app
|-- foo
|   |---a.ts
|   `-- typings.d.ts
`-- tsconfig.json

typing.d.ts 如下:

declare module 'mod' {
    export function add(x: number, y: number): number;
}

a.ts 如下:

import * as Mod from 'mod';

Mod.add(1, 2);

注意,在“a.ts”文件中无法使用相对模块导入来导入外部模块“mod”,示例如下:

import * as M1 from './mod';
//                  ~~~~~~~
//                  错误:无法找到模块'./mod'

import * as M2 from './typings';
//                  ~~~~~~~~~~~
//                  错误:typings.d.ts不是一个模块

2-3. 声明合并

在 TypeScript 中,声明可以有多种含义:

  • 表示一个值
  • 表示一个类型
  • 表示一个命名空间

例如:

const zero = 0; // 声明一值

// 声明一个类型
interface Point {
  x: number;
  y: number;
}

// 声明命名空间
namespace Utils {}

当同一个声明空间内出现同名的标识符,TypeScript 会尝试对其合并——值和值合并,类和类合并,命名空间和命名空间合并。当不能合并时产生编译错误。

2-3-1. interface 声明合并

a. 属性不同合并

interface A {
  a: string;
}

interface A {
  b: number;
}

// 相当于
interface MergedA {
  a: string;
  b: number;
}

b. 同名属性:属性成员必须是相同类型

interface A {
  a: string;
}

interface A {
  a: number; // 编译错误
}

c. 调用签名属性合并——会被视为函数重载,且后声明的有更高的优先级

interface A {
  f(x: any): void;
}

interface A {
  f(x: string): boolean; // 后申明
}

// 相当于
interface MergedA {
  f(x: string): boolean; // 更高优先级
  f(x: any): void;
}

其他情况:带有构造函数签名、接口带有多个字符串索引签名或数值索引签名、泛型接口等,不多赘述。

2-3-2. 枚举声明合并

enum E {
  A,
}
enum E {
  B = 1,
  E, // 正确,自动计算为 2
}
enum E {
  C = 2,
}
enum E {
  D, // 错误,必须为首个成员赋初始值
}

注意,除第一个枚举外,必须为枚举首个成员赋初始值。因为无法在多个枚举成员间自动计算枚举值。

其他情况:必须同时为 const 枚举 或 非const 枚举才能合并。

2-3-3. 类声明合并

不支持同名类合并。

但是外部声明类可以和接口声明合并。

declare class C {
  x: string;
}

interface C {
  y: number;
}

let c: C = new C();
c.x;
c.y;

2-3-4. 命名空间声明合并

a. 与命名空间合并

合并导出成员。

注意:只有导出成员才会合并,非导出成员只能在各自内部使用

namespace Animals {
  export class Bird {}
}

namespace Animals {
  export interface CanFly {
    canFly: boolean;
  }
  export class Dog {}
}

// 相当于
namespace MergedAnimals {
  export interface CanFly {
    canFly: boolean;
  }
  export class Bird {}
  export class Dog {}
}

b. 与函数合并

为函数扩展属性。

注意:要求函数申明在前

function f() {
  return f.version;
}

namespace f {
  export const version = '1.0';
}

f(); // '1.0'
f.version; // '1.0'

c. 与类合并

为类添加静态属性和方法。

注意:要求类声明在前

class A {
  foo: string = A.bar;
}

namespace A {
  export let bar = 'A';
  export function create() {
    return new A();
  }
}

const a: A = A.create();
a.foo(); // 'A'
A.bar(); // 'A'

d. 与枚举合并

将枚举成员与导出成员合并。

注意:要求两种成员不能重名

enum E {
  A,
  B,
  C,
}

namespace E {
  export function foo() {
    E.A;
    E.B;
    E.C;
  }
  export function A () {} // 编译错误!重复的标识符 A
}

E.A;
E.B;
E.C;
E.foo();

2-4. 扩充模块声明

对任意模块已有的声明进行扩展。

假设有如下目录结构的工程:

C:\app

|--a.ts

`--b.ts

export interface A {
  x: number;
}

扩充已有模块,先导入再使用 declare module 扩展。

import { A } from './a';

declare module './a' {
  interface A {
    y: number;
  }
}

const a: A = { x: 0, y: 0 };

注意:

  • 只能扩充原模块已有的声明
  • 只能针对原模块的命名导出

2-4. 扩充全局声明

使用 declare global {}

例:扩展全局 Window 对象,增加 myAppConfig 属性。

export {}; // 内部必须用导出使本身变为一个模块

declare global {
  interface window {
    myAppConfig: object;
  }
}

// 外部直接使用即可
const config: object = window.myAppConfig;

注意:

  • 注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件s

3. 项目配置管理

3-1. 认识 tsc

安装 TypeScript:npm install -g typescript

编译一个文件:tsc index.ts

编译多个文件:tsc index.ts utils.ts

编译一类文件:tsc *.ts

监听编译文件:tsc index.ts -w 或 --watch

查看更多的编译选项:tsc --help --all (详见官网

3-2. tsconfig.json

管理工程中编译文件列表和声明文件列表。

执行 tsc 编译会默认在当前目录查找改配置文件。

{
  // 继承另一个配置文件
  "extends": "./tsconfig.base.json",
  // 编译选项
  "compilerOptions": {
    "strict": true,
    "target": "es5",
    "typeRoots": [], // 会只包含指定含有声明文件的目录,默认"./node_modules/@types","../node_modules/@types","../../node_modules/@types",
    "types": [], // 会只包含指定具体的申明文件
    "composite": true, // 如该工程被其他工程所引用,则必须开启
  },
  "files": [], // 定义只编译哪些文件,
  "include": [], // 定义编译文件列表,支持通配符,如 "src/**/*.ts"。或者直接父级文件夹 "src",
  "exclude": [], // 从 include 列表中去除指定文件,
  // 如该工程被其他工程所依赖,则需指定当前工程所引用的其他工程目录
  "references": [
    { "path": "../pkg1" }, // 可以是含有tsconfig.json的目录
    { "path": "../pkg2/tsconfig.release.json" }, // 也可以是具体的配置文件
  ],
}