# 申明文件概述

在Typesript 中使用 Array, Object 或 document,localStorage 等 EMACScript 内建对象或浏览器 API 时,Typescirpt 会自动提示。这是由于 Typescript 内部定义了一系列申明文件来处理默认的内建对象和宿主对象,这正是之前在 对象类型-内建对象 中讨论的 Typesciprt 官方仓库 lib (opens new window) 目录下 .d.ts 结尾的文件作用。这些以 .d.ts 结尾的文件叫做申明文件。

申明文件除了帮助 Typescript 定义 js 规范中的对象类型,也可帮助开发者定义 npm 包等三方模块的类型,确保他人在使用这些包时,Typescript 可以正确识别包中的类型定义。

# 定义申明文件

参见 https://devblogs.microsoft.com/typescript/writing-dts-files-for-types/

# 全局申明

全局申明,当导入第三方库时可能可能会在全局环境绑定。变量和方法。 通过 declare 语句提前申明这些变量,避免 TypeScript 检查错误。

// 声明全局变量 gVar
declare var gVar: number;
// 申明全局函数
declare function greet(str: string): void;
// 申明全局常量
declare const foo2: string;

gVar = 1;
console.log(gVar);

注意全局的常量和函数在这里只进行了申明,定义参见

# 申明文件类型

  • 内建类型申明 Typescript 源码中 lib (opens new window) 对 js 内建对象和浏览器相关 API 的支持,通常以 lib.[something].d.ts 命名,这些定义集成了开发者常用 js 对象的类型检查。
  • 外部定义 非 Typescirpt 内建的申明文件,例如外部引入的 npm 包等其他模块的定义。根据使用场景分为如下几种:
    • Bundled Types 以 js 作为结果输出的 npm 包,后续会详细讲解,如何对这类包创建声明文件,避免 Typescript 引入报错。

    • DefinitelyTyped / @types Typescript 官方为常用的 npm 包定义的申明文件,github 仓库为 DefinitelyTyped/DefinitelyTyped (opens new window), 可以直接通过 npm install --save-dev @types/lodash 安装对应申明,Typescript 会自动引入 node_modules/@types 目录的类型定义

      TIP

      在使用三方仓库时可以通过 npm info @types/<包名> 查找是否已有相关类型申明,例如 npm info @types/lodash 会输出 @types/lodash 包的信息。然后安装对应的申明包即支持 Typescript 类型检查。

    • 自定义申明 当引用的模块没有对应的申明文件时,需要自行定义相关的申明文件,避免 Typescript 检查报错。

# 使用和发布申明文件

当发布自定义包或引入第三方的包时,如何暴露或引用类型申明,方法如下

# 自定义包的处理

若采用 TypeScript 编写的应用编译时添加 --declaration 会自动产生申明文件。

对于 js 编写的包, TypeScript 会利用如下方式解析申明文件。

  1. 若包根目录定义了 index.d.ts 会被解析
  2. 若 package.json 定义了 files 字段自行编写申明文件后,加入此字段
  3. 若未定义则需在 package.json 使用 types 或 typings 字段包含申明文件。

# 第三方包

去 types (opens new window) 搜索第三方包,安装对应的类型申明。

若未找到申明文件则参考 DefinitelyTyped (opens new window) 定义类型包提交到社区。

# 配置

# reference (opens new window)

显示添加申明文件

# 最佳实践

# 内置申明文件

Including built-in type declarations with --lib (opens new window)

# 申明合并 (declaration merging)

申明合并 是指 TypeScript 编译器合并两个或多个具有相同名字的独立申明为一个单独定义。

interface Foo {
  a: string;
}
interface Foo {
  b: number;
}
interface Foo {
  c: number;
}

/**
 * TyeScript 检测到 Foo 有三个同名的申明,合并为一个申明
 * interface Foo {
 *  a: string;
 *  b: number,
 *  c: number
 * }
 *  */

// foo1 必须具备合并后的类型所具有的特征
let foo1: Foo = {
  a: 'foo',
  b: 1,
  c: 1
};

TypeScript 申明会创建如下类型的实体。

  • NameSpace 一个命名空间,在该命名空间下采用 . 访问符获取类型值等信息
  • Type 一个类型
  • Value 在 JavaScript 中可见的值

参见下图理解,不同申明创建的结果。

Declaration Type Namespace Type Value
Namespace X X
Class X X
Enum X X
Interface X
Type Alias X
Function X
Variable X

理解不同申明会创建什么可以帮助你更好的理解哪些内容将会在申明合并时合并。

# 接口合并顺序

接口申明合并遵循如下规则。

1. 每个接口的申明后申明的,合并后放在前面,并保持各接口内部顺序。

interface Salute {
  hi(str: string): void;
}

interface Salute {
  hi(str: string, name: string): void;
}

/**
 * 声明会合并为
 * interface Salute {
 *  hi(str: string, name: string): void; // 后申明的函数在前面
 *  hi(str: string): void;
 * }
 */

2. 如果函数表示中包含单个字符串字面量(不是字符串字面量的联合类型),合并后申明顺序会在规则 1 的基础上全部提前

interface Document {
  createElement(tagName: any): Element;
}
interface Document {
  // 由于包含单个字符串字面量该申明会在合并后,提前 2
  createElement(tagName: 'div'): HTMLDivElement;
  // 由于包含单个字符串字面量该申明会在合并后,提前 3
  createElement(tagName: 'span'): HTMLSpanElement;
}
interface Document {
  createElement(tagName: string): HTMLElement;
  // 由于包含单个字符串字面量该申明会在合并后,提前 1
  createElement(tagName: 'canvas'): HTMLCanvasElement;
}

/**
 * 合并后结果,其余保持默认顺序
 * interface Document {
    createElement(tagName: "canvas"): HTMLCanvasElement; // 提前 1
    createElement(tagName: "div"): HTMLDivElement; // 提前 2
    createElement(tagName: "span"): HTMLSpanElement; // 提前 3
    createElement(tagName: string): HTMLElement;
    createElement(tagName: any): Element;
  } 
 */

# 命名空间合并

命名空间合并类似接口。具有如下规则

1. 只合并包含 export 的申明

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 { }
}
 */

2. 未导出只能在各命名空间内部访问

namespace Animal {
  let haveMuscles = true;

  export function animalsHaveMuscles() {
    return haveMuscles;
  }
}

namespace Animal {
  export function doAnimalsHaveMuscles() {
    // 无法访问
    return haveMuscles;
  }
}

# namespace 结合 class,function 和 枚举

利用 namespace 相当于给类扩充了静态方法或属性。

class Album {
  label: Album.AlbumLabel;
  foo = Album.Foo;
}
namespace Album {
  export const Foo = 1;
  export class AlbumLabel {}
}

同样也可以扩充函数的属性合法和方法

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

// 在枚举中混入方法,
console.log(Color.mixColor('yellow'));

# 模块扩充 (Module Augmentation)

有时需要对导入模块注入新的方法,可以采用 declare module 模块名的方式 添加新的类型申明。

module.ts

export class Observable<T> {}

index.ts

import { Observable } from './module1';

// 采用此申明在导入模块 Observable 上扩充 map 方法,避免使用报错
declare module './module1' {
  interface Observable<T> {
    map(): void;
  }
}

Observable.prototype.map = function() {};

注意使用时的两个限制

  1. 扩充的模块名必须已经存在
  2. 不能扩充 default 导出的内容

同样也可在全局空间进行扩充

export function toObservable1() {}

// 全局扩充, 将模块方法扩充到全局的原型对象上
declare global {
  interface Array<T> {
    toObservable1(): void;
  }
}

Array.prototype.toObservable1 = toObservable1;

全局扩充只能在如下条件存在

  1. 模块文件内,(TypeScript 检测到模块引入和导出语法就判别为模块)
  2. 申明文件内

资料参考 [官方申明合并](申明合并 https://www.typescriptlang.org/docs/handbook/declaration-merging.html)

# 总结

本小结我们 Typescript 中以 .d.ts 结尾的申明文件的使用。知识点如下。

  1. Typescirpt 中以 .d.ts 结尾的文件叫做 申明文件,具有如下功能
    1. 用于 Typescript 内部定义 js 及相关规范中的内建对象和宿主对象类型
    2. 用于定义包的导出类型,帮助 Typescript 识别导入的三方模块的类型

# 延伸阅读