- 文档
- 教程
【免费下载链接】TypeScript
TypeScript 使用手册(中文版)翻译。http://www.typescriptlang.org
本文是 TypeScript 使用手册(中文版)"声明文件·模板"系列的技术指南,围绕全局代码库(Global Library)这一库形态,讲解如何识别它、如何为它编写
global.d.ts声明文件,并逐段剖析官方模板的每个声明构造。读完本文,你将能独立判断一个 JavaScript 库是否属于全局代码库、在动手前排除 UMD 陷阱,并依据模板写出类型安全、可发布、可复用的全局声明文件。
什么是全局代码库
全局代码库是指可以通过全局作用域直接访问的代码库——即不使用任何形式的import语句就能使用它。许多代码库只是简单地导出一个或多个供使用的全局变量。
例如,如果你使用 jQuery,那么可以直接通过$变量来引用它,无需任何导入语句:
$(() => { console.log('hello!'); });从使用方式上看,你通常能够在代码库的文档里看到如何在 HTML 的script标签里引用它:
<script src="http://a.great.cdn.for/someLib.js"></script><script>标签将代码加载进页面后,其中定义的全局变量(如上面的$)即可在后续所有脚本中直接访问。这正是全局代码库与模块化代码库最根本的使用差异:前者依赖浏览器全局作用域,后者依赖模块加载器。
在动手编写声明文件之前,请务必确认一点:目前大多数流行的全局代码库都以 UMD 代码库的形式发布,而 UMD 代码库与全局代码库很难通过文档来区分。因此,编写全局代码库的声明文件之前,必须先确认目标代码库不是UMD 代码库(UMD 的判断方法与模板见后文)。
从代码识别全局代码库
通常,全局代码库的源码十分简单。一个全局的 "Hello, world" 代码库可以是这样:
function createGreeting(s) { return 'Hello, ' + s; }或者这样:
window.createGreeting = function (s) { return 'Hello, ' + s; };在阅读全局代码库的代码时,你会看到以下特征:
- 顶层的
var语句或function声明; - 一个或多个
window.someName赋值语句; - 假设 DOM 相关的原始值
document或window存在(即代码直接使用它们,而非通过导入获得)。
而下面这些特征不会出现在真正的全局代码库中:
- 检查或使用了模块加载器,如
require或define; - CommonJS / Node.js 风格的导入语句,如
var fs = require("fs");; define(...)调用;- 描述
require或导入代码库的文档。
这些"正反特征"是判断代码库形态的速查表:正面命中顶层声明、window赋值、DOM 依赖,反面完全没有模块加载器痕迹,那么它就是一个全局代码库。相关内容在《代码库结构》一节中也有完整的对应说明,可作为交叉验证。
一个重要的反例:UMD 代码库
UMD 模块既可以用作 ES 模块(使用导入语句),也可以用作全局变量(在缺少模块加载器的环境中使用)。识别它的关键是源码顶端的环境探测代码:
(function (root, factory) { if (typeof define === "function" && define.amd) { define(["libName"], factory); } else if (typeof module === "object" && module.exports) { module.exports = factory(require("libName")); } else { root.returnExports = factory(root.libName); } }(this, function (b) {如果你看到代码库中存在类似typeof define、typeof window或typeof module的检测代码——尤其是在文件顶端——那么它大概率是 UMD 代码库,此时应改用模块模板或 module-plugin 模板,而不是全局代码库模板。
全局代码库的现状
由于将全局代码库转换为 UMD 代码库十分容易,如今很少有代码库仍然保持全局代码库风格。不过,小型的代码库以及需要使用 DOM 的代码库仍然可以是全局的——例如直接操作document、window的工具库,或通过 CDN<script>引入的轻量插件。这类库正是global.d.ts模板的适用对象。
全局代码库模板逐段解析
模板文件 global.d.ts 以myLib为例,完整展示了为全局代码库编写声明文件的骨架。下面逐段解析每一部分的作用,并给出可直接套用、可复制的完整模板:
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] // Project: [~THE PROJECT NAME~] // Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> /*~ If this library is callable (e.g. can be invoked as myLib(3)), *~ include those call signatures here. *~ Otherwise, delete this section. */ declare function myLib(a: string): string; declare function myLib(a: number): number; /*~ If you want the name of this library to be a valid type name, *~ you can do so here. *~ *~ For example, this allows us to write 'var x: myLib'; *~ Be sure this actually makes sense! If it doesn't, just *~ delete this declaration and add types inside the namespace below. */ interface myLib { name: string; length: number; extras?: string[]; } /*~ If your library has properties exposed on a global variable, *~ place them here. *~ You should also place types (interfaces and type alias) here. */ declare namespace myLib { //~ We can write 'myLib.timeout = 50;' let timeout: number; //~ We can access 'myLib.version', but not change it const version: string; //~ There's some class we can create via 'let c = new myLib.Cat(42)' //~ Or reference e.g. 'function f(c: myLib.Cat) { ... } class Cat { constructor(n: number); //~ We can read 'c.age' from a 'Cat' instance readonly age: number; //~ We can invoke 'c.purr()' from a 'Cat' instance purr(): void; } //~ We can declare a variable as //~ 'var s: myLib.CatSettings = { weight: 5, name: "Maru" };' interface CatSettings { weight: number; name: string; tailLength?: number; } //~ We can write 'const v: myLib.VetID = 42;' //~ or 'const v: myLib.VetID = "bob";' type VetID = string | number; //~ We can invoke 'myLib.checkCat(c)' or 'myLib.checkCat(c, v);' function checkCat(c: Cat, s?: VetID); }头部注释与占位符
模板前三行是声明文件的元信息注释,其中的[~THE LIBRARY NAME~]、[~THE PROJECT NAME~]、[~YOUR NAME~] <[~A URL FOR YOU~]>均为占位符,分别应替换为:声明文件所描述代码库的名称(可附版本号)、代码库所属项目名称、维护者姓名与可联系的 URL。这与《发布》一节中提到的@types包维护约定一致。
可调用库:declare function重载
declare function myLib(a: string): string; declare function myLib(a: number): number;如果该库可以被直接调用(例如myLib(3)),就在这里声明它的调用签名。注意模板使用了两条重载,分别处理string与number两种入参——这是声明文件中对函数重载的标准表达方式,与《举例》一节中getWidget的重载示例完全一致。如果库不可调用,请删除这一节。
库名作为类型名:interface
interface myLib { name: string; length: number; extras?: string[]; }如果希望myLib这个名字本身可以充当类型名(例如允许写var x: myLib),可以在此声明一个同名接口。模板注释提醒:请确保这样声明确实有意义——只有当库的真实 API 确实是"一个具有这些属性的对象"时才保留它;否则应删除该声明,并把需要的类型放进下面的命名空间里。
全局变量的属性与类型:declare namespace
declare namespace myLib { let timeout: number; const version: string; // ...class / interface / type / function... }如果代码库在全局变量上暴露了属性(例如myLib.timeout、myLib.version),就将它们放进declare namespace中。这是全局代码库声明文件的核心组织方式:命名空间既承载值(let/const/class/function),也承载类型(interface/type),使用者通过点号访问,例如myLib.Cat、myLib.VetID。
模板中各个声明成员的语义逐一解读如下:
| 声明 | 使用效果 | 说明 |
|---|---|---|
let timeout: number; | 可以写myLib.timeout = 50; | 属性可读写 |
const version: string; | 可以读myLib.version,但不能修改 | 属性只读 |
class Cat | let c = new myLib.Cat(42),function f(c: myLib.Cat) | 构造函数 + 实例成员:readonly age: number只读属性、purr(): void方法 |
interface CatSettings | var s: myLib.CatSettings = { weight: 5, name: "Maru" }; | 含可选属性tailLength?: number |
type VetID = string \| number; | const v: myLib.VetID = 42;或= "bob"; | 联合类型别名 |
function checkCat(c: Cat, s?: VetID); | myLib.checkCat(c)或myLib.checkCat(c, v) | 可选参数 |
命名空间背后的类型/值/命名空间三义性
从《深入》一节的原理看,declare namespace myLib之所以能同时容纳"值"(timeout、Cat构造函数)与"类型"(CatSettings、VetID),是因为 TypeScript 中一个名字可以承载三种不同的意义:类型、值、命名空间。例如class Cat同时创建了"类型Cat"(指向实例结构)与"值Cat"(指向构造函数),二者名字相同却不冲突。这正是全局代码库声明文件能在一个myLib名下完整描述复杂 API 的底层机制。
防止命名冲突:把类型放进命名空间
在全局作用域里虽然可以定义许多类型,但强烈不建议这样做——当一个工程中存在多个声明文件时,全局顶层类型很容易导致难以解决的命名冲突。这一点在《代码库结构》的脚注"防止命名冲突"中有明确告诫。
可以遵循的简单规则是:使用代码库提供的某个全局变量来声明拥有命名空间的类型。例如,如果代码库提供了全局变量cats,应该这样写:
declare namespace cats { interface KittySettings {} }而不是在全局顶层这样写:
// at top-level interface CatsKittySettings {}这样组织的好处是:保证代码库将来可以被转换为 UMD 模块,且不会影响声明文件的使用者——所有类型都收纳在cats.前缀之下,命名空间天然隔离了冲突。
全局声明文件中的依赖处理
你的全局代码库声明文件可能还会依赖其他代码库。依据《代码库结构》的"利用依赖"一节,依赖声明方式取决于依赖方的形态:
依赖某个全局代码库:使用
/// <reference types="..." />指令:/// <reference types="someLib" /> function getThing(): someLib.thing;依赖某个模块:使用
import语句(注意:模块导入会改变文件的"全局"性质,需结合实际情况权衡):import * as moment from 'moment'; function getThing(): moment;全局代码库依赖某个 UMD 模块:同样使用
/// <reference types="moment" />指令,随后即可在全局声明中引用moment暴露出的类型。反之,模块或 UMD 代码库依赖 UMD 代码库时应使用import语句,此时不要使用/// <reference指令。
此外,发布声明文件时(详见《发布》),不要在声明文件里使用/// <reference path="..." />(文件路径引用),而应使用/// <reference types="..." />(包名引用),并确保被依赖的声明包写在package.json的"dependencies"(而非devDependencies)中,以便使用你声明文件的下游用户自动获得这些依赖。
与其他模板的边界:global、plugin 与 modifying-module
全局代码库模板只是《模板》七件套之一。为选对模板,需要区分三种容易混淆的全局相关形态:
| 模板 | 适用对象 | 关键差异 |
|---|---|---|
| global.d.ts | 全局代码库 | 本身通过全局作用域使用,无import参与 |
| global-plugin.d.ts | 全局插件 | 一段全局代码,修改全局对象结构(如给String.prototype、Array.prototype添加方法) |
| global-modifying-module.d.ts | 修改全局作用域的模块 | 需要require/import激活,导入时修改全局作用域(如给String.prototype添加成员),模板中使用declare global { ... }块 |
全局插件与"修改全局作用域的模块"的文档示例通常是这样的——给内置类型添加新方法:
var x = 'hello, world'; // Creates new methods on built-in types console.log(x.startsWithHello()); var y = [1, 2, 3]; // Creates new methods on built-in types console.log(y.reverseAndSort());二者的区别在于激活方式:全局插件通过<script>加载即生效;修改全局作用域的模块则需要一行不接收返回值的require('magic-string-time')来激活副作用。这种模式存在运行时冲突的风险,但在明确的前提下仍可为其编写声明文件——通过declare global将声明注入全局命名空间,其底层机制与《声明合并》中"全局扩展"一节描述的行为一致:在模块内部用declare global { interface Array<T> { ... } }扩充内置类型,扩展内容与原始声明合并,但不能声明新的顶层声明,只能扩展已存在的声明。
与之相对,如果库既是模块又希望暴露全局变量名,则属于 UMD 形态,应在module.d.ts 模板中使用export as namespace myLib;来声明全局名,而不是使用全局代码库模板。
从模板到真实项目:发布与使用
声明文件写好后,有两条发布路径(详见《发布》):
- 与 npm 包捆绑发布:在
package.json中通过"types"字段指向声明文件("typings"与"types"意义相同);若主声明文件恰好是包根目录下的index.d.ts(与index.js并列),则无需显式指定。 - 提交到 DefinitelyTyped:由社区统一发布为
@types/xxx包,使用者通过npm install --save @types/xxx安装(详见《使用》)。
对使用者而言,安装好声明文件后,无论是通过import导入还是直接在全局作用域使用_,TypeScript 都能获得完整的类型提示与编译期检查。
小结
global.d.ts模板解决的核心问题是:在无模块系统的场景下,如何为通过全局作用域暴露的代码库提供完整的类型描述。本文覆盖了全局代码库的定义与识别(顶层声明、window赋值、DOM 依赖为正特征;require/define/模块加载器为反特征)、UMD 陷阱的排除、模板中declare function(可调用)、interface(类型名)、declare namespace(属性与类型收纳)三大构造的逐段用法,以及命名冲突规避、依赖声明、模板边界判定与发布消费链路。建议结合《代码库结构》的库形态全景和《举例》的 API 模式示例一起阅读,形成从"识别库类型"到"写出声明文件"的完整方法论。
- 文档
- 教程
【免费下载链接】TypeScript
TypeScript 使用手册(中文版)翻译。http://www.typescriptlang.org
相关推荐
企业级客服机器人语义理解实践指南:如何利用GTE-large-zh提升智能客服效果
企业级客服机器人语义理解实践指南:如何利用GTE large zh提升智能客服效果 在当今数字化时代, GTE large zh 作为阿里巴巴达摩院开发的高性能
文档教程QtScrcpy:3分钟实现安卓设备跨平台高清投屏控制
QtScrcpy:3分钟实现安卓设备跨平台高清投屏控制 QtScrcpy是一款基于Qt框架开发的Android实时显示控制软件,通过USB或网络连接,无需roo
文档教程Gatsby Monorepo 全局 TypeScript 声明注入机制解析:以 types/gatsby-monorepo 的 global.d.ts 为例
Gatsby Monorepo 全局 TypeScript 声明注入机制解析:以 types/gatsby monorepo 的 global.d.ts 为例
前端静态站点Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考