本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-21
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全,接口提示等功能。
当我们使用 jQuery
时,ts 编译器并不知道 $
或 jQuery
是什么对象,什么类型。所以需要定义它的类型。
declare var jQuery: (selector: string) => any;
declare var
并没真正定义一个变量,只是定义了全局变量 jQuery
的类型,仅仅会用于编译时的检查,在编译结果中会被删除。
声明语句有以下几种:
declare var
: 声明全局变量declare function
: 声明全局方法declare class
: 声明全局类declare enum
: 声明全局枚举类型declare namespace
: 声明(含有子属性的)全局对象interface
和 type
: 声明全局类型export
: 导出变量export namespace
: 导出(含有子属性的)对象export default
: ES6 默认导出。export =
: commonjs 导出模块。export as namespace
: UMD 库声明全局变量declare global
: 扩展全局变量declare module
: 扩展模块/// <reference />
: 三斜线指令通常会把声明语句放到一个单独的文件中,以 .d.ts
为后缀,这就是声明文件。 一般来说,ts 会解析项目中所有 *.ts
文件,当然也包含以 d.ts
结尾的文件,所以当我们将类似 jQuery.d.ts
文件放到项目中,其他所有 *.ts
文件就都可以获得 jQuery
的类型定义了。
假如仍然无法解析,那么可以检查一下 tsconfig.json
中的 files
、include
和 exclude
配置,确保其包含了 jQuery.d.ts
文件。
很多第三方库都由其官方帮我们定义好了,我们使用@types
去下载管理即可,可以在这个页面搜索你需要的声明文件。
npm install @types/jquery --save-dev
当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了;在不同的场景下,声明文件的内容和使用方式会有所不同。库的使用场景主要有以下几种:
通过<script>
标签引入第三方库,注入全局变量。如上面提到的jQuery
,就可以使用安装@type/jQuery
来引用声明文件,也可以自己书写 .d.ts
文件,使用 declare
语句。
全局库是指能在全局命名空间下访问的(不需要使用任何形式的import)。许多库都是简单的暴露出一个或多个全局变量。 比如 jQuery。
当你查看全局库的源代码时,你通常会看到:
var
语句或function
声明window.someName
document
或window
是存在的用来定义一个全局变量的类型,同时还有 declare let
和 declare const
,需要注意的是,当我们使用 declare const
定义时,表示此时的全局变量是一个常量,不允许再去修改它的值了。
declare const jQuery: (selector: string) => any;
jQuery = function(selector) {
return document.querySelector(selector);
}
// Error “Cannot assign to 'jQuery' because it is a constant or a read-only property.”
一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用 const
而不是 var
或 let
。
另一个需要注意的是,声明语句中只能定义类型,切勿在声明语句中定义具体的实现。
declare const jQuery = function(selector) {
return document.querySelector(selector);
}
// Error “An implementation cannot be declared in ambient contexts.”
用来定义全局函数,jQuery
其实就是一个函数,所以也可以用 function
来定义。
declare function jQuery(selector: string): any;
// 使用declare function 可以进行重载
declare function jQuery(domReadyCallback: () => any): any;
用来定义一个全局类。
declare class Animal {
name: string;
constructor(name: string)
}
// 使用
const cat = new Animal('Tom');
用来定义枚举类型。
declare enum Directions {
Up,
Down,
Left,
Right
}
// 同样 declare enum 也是定义类型,而不是具体的值
比如,jQuery 既是一个函数,可以直接调用 jQuery('#foo')
,又是一个对象,拥有子属性 jQuery.ajax('/api/get_something')
,那么我们可以组合多个声明语句。
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, setting?: any): void;
}
使用 declare namespace
定义一个对象的属性的时候可以这样定义:
// 如果您的库具有公开在全局变量中的属性 把它们放在这里 您还应该在此处放置类型(接口和类型别名)
declare namespace myLib {
// 这样可以写 'myLib.timeout = 50;'
let timeout: number;
// 这样可以访问 'myLib.version', 但不能更改
const version: string;
// 这样可以通过'let c = new myLib.Cat(42)'创建一些类
// 或引用,例如 function f(c:myLib.Cat){...}
class Cat {
constructor(n: number);
// 这样可以从 Cat 实例 c 中读取 c.age
readonly age: number;
// 这样可以通过 c.purr() 调用方法
purr(): void;
}
// 定义这种,这样可以声明变量
// 'var s: myLib.CatSettings = { weight: 5, name: "Maru" };'
interface CatSettings {
weight: number;
name: string;
tailLength?: number;
}
// 这样可以写 'const v: myLib.VetID = 42;'
// 或者 'const v: myLib.VetID = "bob";'
type VetID = string | number;
// 这样可以调用 'myLib.checkCat(c)' 或者 'myLib.checkCat(c, v);'
function checkCat(c: Cat, s?: VetID);
}
模块库至少会包含下列具有代表性的条目之一:
require
或define
import * as a from 'b';
or export c;
这样的声明exports
或module.exports
window
或global
的赋值在我们尝试给一个模块化库创建声明文件之前,需要先看看它的声明文件是否已经存在。一般来说,模块化库的声明文件可能存在与两个地方:
package.json
中的 types
字段,或者在根目录有一个 index.d.ts
。
发布到 @types
里,我们只需尝试安装一下对应的 @types
包就知道是否存在该声明文件,npm install @types/foo --save-dev
。
假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。 由于是通过 import
语句导入的模块,所以声明文件存放的位置也有所约束。
创建一个 types
目录,专门用来管理自己写的声明文件,将 foo
声明文件放到 types/foo/index.d.ts
中,这种方式需要配置 tsconfig.json
中的 paths
和 baseUrl
字段。
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}
模块化库的声明文件主要有以下几种语法:
export
:导出变量export namespace
: 导出(含有子属性)对象export default
: 默认导出export =
: commonjs 导出模块在为 NPM 包书写声明文件时,使用 declare
不再会声明一个全局变量,而只会在当前文件声明一个局部变量,只有在声明文件中使用 export
导出,然后在使用方 import
导入后,才会应用到这些类型声明。
// types/foo/index.d.ts
export const name: string;
export function getName(): string;
export class Animal {
constructor(name: string);
sayHi(): string;
}
export enum Directions {
Up,
Down,
Left,
Right
}
export interface Options {
data: any;
}
我们也可以使用 declare
先声明多个变量,最后在使用 export
一次性导出。
// types/foo/index.d.ts
declare const name: string;
declare function getName(): string;
interface Options {
data: any;
}
export { name, getName, Options }
与 declare namespace
类似,export namespace
用来导出一个拥有子属性的对象。
// types/foo/index.d.ts
export namespace foo {
const name: string;
namespace bar {
function baz(): string;
}
}
// src/index.ts
import { foo } from 'foo';
console.log(foo.name);
foo.bar.baz();”
在 ES6 模块系统中,使用 export default
可以导出一些默认值,使用方可以用 import foo from 'foo'
导入。
// types/foo/index.d.ts
export default function foo(): string;
// src/index.ts
import foo from 'foo';
foo();
需要注意的是,在使用 export default
时只有 function
、class
和 interface
可以直接默认导出,其他的变量需要先使用 declare
定义,再默认导出。
在 commonjs 规范中,我们用以下方式导出模块。
// 整体导出
module.exports = foo;
// 单个导出
exports.bar = bar;
在 ts 中,针对这种模块导出,有多种方式可以导入:
// 1、使用 require
// 整体导入
const foo = require('foo');
// 单个导入
const bar = require('foo').bar;
// 2. 使用 import from
// 整体导入
import * as foo from 'foo';
// 单个导入
import { bar } from 'foo';”
// 3. 使用 import require,也是ts官方推荐的方式
// 整体导入
import foo = require('foo');
// 单个导入
import bar = foo.bar;
对于这种使用 commonjs 规范的库,如果要写声明文件就需要使用到 export =
这种语法了
// types/foo/index.d.ts
export = foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
// 使用 `export =` 之后就不能再单个导出了 `export { bar }` 了
// 所以通过声明合并,使用 `declare namespace foo` 来将 bar 合并到 foo 里
既可以通过 <script>
标签引入,又可以通过 import
导入的库。
UMD模块会检查是否存在模块加载器环境。 这是非常形容观察到的模块,它们会像下面这样:
如果你在库的源码里看到了typeof define
,typeof window
,或typeof module
这样的测试,尤其是在文件的顶端,那么它几乎就是一个UMD库。
UMD库的文档里经常会包含:
require
在Node.js里使用例子<script>
标签去加载脚本。相比于 NPM 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式, ts 提供了一个 export as namespace
语法。
一般使用 export as namespace
时,都是先有了 NPM 包的声明文件,再基于它添加一条 export as namespace
,即可将声明好的一个变量声明为全局变量。
// types/foo/index.d.ts
export as namespace foo;
export = foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
或者,与 export default
一起使用
// types/foo/index.d.ts
export as namespace foo;
export default foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
如果模块能够作为函数调用:
var x = require("foo");
var y = x(42);
// 如果您正在为 super-greeter 编写文件,
// 则此文件应为 super-greeter/index.d.ts 并将其放置在与模块同名的文件夹中。
// 如果此模块是有公开全局变量 'myFuncLib' 的UMD模块,
// 可以在模块加载器环境之外加载 ,则需要在这里声明全局变量。
// 否则,请删除此声明。
export as namespace myFuncLib;
// 该声明指定该函数是文件中的导出对象
export = MyFunction;
// 本示例说明如何为您的函数产生多个重载
declare function MyFunction(name: string): MyFunction.NamedReturnType;
declare function MyFunction(length: number): MyFunction.LengthReturnType;
// 如果您还想公开模块中的类型,则可以将它们放在此块中
// 该类型应如此处示例所示这样声明
declare namespace MyFunction {
export interface LengthReturnType {
width: number;
height: number;
}
export interface NamedReturnType {
firstName: string;
lastName: string;
}
// 如果模块也具有属性,请在此处声明它们。 例如,此声明表明此代码是合法的:
// import f = require('myFuncLibrary');
// console.log(f.defaultName);
export const defaultName: string;
export let defaultLength: number;
}
如果模块能够使用new来构造:
var x = require("bar");
var y = new x("hello");
// 如果是公开全局变量请这样定义,否则删除这一行
export as namespace myClassLib;
// 该声明指定该函数是文件中的导出对象
export = MyClass;
// 在此类中编写模块的方法和属性
declare class MyClass {
constructor(someParam?: string);
someProperty: string[];
myMethod(opts: MyClass.MyClassMethodOptions): number;
}
// 如果您还想公开模块中的类型,则可以将它们放在此块中
declare namespace MyClass {
export interface MyClassMethodOptions {
width?: number;
height?: number;
}
}
如果模块不能被调用或构造:
export as namespace myLib;
// 如果此模块具有方法,则将它们声明为如下:
export function myMethod(a: string): string;
export function myOtherMethod(a: number): number;
// 声明可用的类型
export interface someType {
name: string;
length: number;
extras?: string[];
}
// 可以使用const,let或var声明模块的属性
export const myField: number;
// namespace 中声明拥有子属性的对象。
export namespace subProp {
export function foo(): void;
}
// 如果这样定义 subProp 和 foo 就可以这使用
// import { subProp } from 'yourModule';
// subProp.foo();
// 或者
// import * as yourMod from 'yourModule';
// yourMod.subProp.foo();
一个全局插件是全局代码,通过<script>
标签引入第三方库后,改变一个全局对象的结构。比如,一些库往Array.prototype
或String.prototype
里添加新的方法。
比如可以如下调用:
var x = "hello, world";
console.log(x.startsWithHello());
// 编写原始类型的声明并添加新成员。
// 例如,这将添加一个'toBinaryString'方法,以将重载到内置数字类型。
interface Number {
toBinaryString(opts?: MyLibrary.BinaryFormatOptions): string;
toBinaryString(callback: MyLibrary.BinaryFormatCallback, opts?: MyLibrary.BinaryFormatOptions): string;
}
// 如果需要声明几种类型,请将它们放在名称空间中以避免向全局名称空间添加太多内容
declare namespace MyLibrary {
type BinaryFormatCallback = (n: number) => string;
interface BinaryFormatOptions {
prefix?: string;
padding: number;
}
}
也可以使用 declare namespace
给已有的命名空间添加类型声明。
// types/jquery-plugin/index.d.ts
declare namespace JQuery {
interface CustomOptions {
bar: string;
}
}
interface JQueryStatic {
foo(options: JQuery.CustomOptions): string;
}
// src/index.ts
jQuery.foo({
bar: ''
});
引用NPM包或UMD库后,改变一个全局变量的结构。通常来讲,它们与全局插件相似,但是需要require
调用来激活它们的效果。
require("magic-string-time"); // 激活
var x = "hello, world";
console.log(x.startsWithHello());
对于一个NPM包或者UMD库的声明文件,只有 export
导出的类型声明才能被导入,所以对NPM包或UMD库,如果导入此库之后会扩展你全局变量,则需要使用另外一种语法 declare global
。
// types/foo/index.d.ts
declare global {
interface String {
prependHello(): string;
}
}
export {};
注意即使此声明文件不需要导出任何东西,仍然需要使用export {}
导出一个空对象,用来告诉编译器这是一个模块(NPM包或UMD库扩展)的声明文件,而不是一个全局变量(<script>
引入直接修改)的声明文件。
通过 <script>
或 import
导入后,改变另一个模块的结构。
有时通过 import
导入一个模块插件,可以改变另外一个原有模块的结构,比如 vuex
相较于 vue
。此时如果原有模块已经有了类型声明,而插件模块没有类型声明,就会导致类型不完整,缺少插件部分的类型。ts 提供了 declare module
语法,它可以扩展原有模块的类型。
如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 declare module
扩展原有模块。
// types/moment-plugin/index.d.ts
// 导入此模块添加到的模块
import * as moment from 'moment';
// 也可以导入你所需要的其他模块
import * as other from 'anotherModule';
declare module 'moment' {
export function foo(): moment.CalendarKey;
// 还可以为原始模块已经存在的接口 通过重写接口 来新增属性
export interface SomeModuleOptions {
someModuleSetting?: string;
}
// 也可以声明新类型,并且新类型看起来就像在原始模块中一样
export interface MyModulePluginOptions {
size: number;
}
}
通常这么使用:
// src/index.ts
import * as moment from 'moment';
import 'moment-plugin';”
moment.foo();
declare module
也可用于在一个文件中一次性声明多个模块的类型。
// types/foo-bar.d.ts
declare module 'foo' {
export interface Foo {
foo: string;
}
}
declare module 'bar' {
export function bar(): string;
}
// src/index.ts
import { Foo } from 'foo';
import * as bar from 'bar';
let f: Foo;
bar.bar();
一个声明文件有时会依赖另一个声明文件中的类型,可以在声明文件中导入另一个声明文件中的类型。
import * as moment from 'moment';
declare module 'moment' {
export function foo(): moment.CalendarKey;
}
/// <reference types="someLib" />
function getThing(): someLib.thing;
如果你的全局库依赖于某个UMD模块,使用/// <reference types
指令:
/// <reference types="moment" />
function getThing(): moment;
如果你的模块或UMD库依赖于一个UMD库,使用import
语句:
import * as someLib from 'someLib';
除了 import
外,还有一个语法可以用来导入另一个声明文件,那就是三斜线指令。虽然现在不常使用,但可能读文档的时候会遇到。
它与 import
的区别是,当且仅当在以下几个场景下,我们才需要使用三斜线指令替代 import
:
a. 当我们在书写一个全局变量的声明文件时
在全局变量的声明文件中,是不允许出现 import
、export
关键字的,一旦出现了,那么就会被视为一个 NPM 包或 UMD 库,就不再是全局变量的声明文件了,故当我们在书写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令了。
/// <reference types="jquery" />
declare function foo(options: JQuery.AjaxSettings): string;
// 书写全局变量的声明文件,却需要引用jquery库的类型
需要注意的是,///
指令必须放在文件的最顶端,即三斜线指令的前面只允许出现单行或多行注释
b. 当我们需要依赖一个全局变量的声明文件时
/// <reference types="node" />
export function foo(p: NodeJS.Process): string;
// 由于引入的node中的类型都是全局变量的类型,它们没有办法通过import来引入
当我们的全局变量的声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将它们一一引入,来提高代码的可维护性。
// 比如 jQuery 的声明文件就如下
/// <reference types="sizzle" />
/// <reference path="JQueryStatic.d.ts" />
/// <reference path="JQuery.d.ts" />
/// <reference path="misc.d.ts" />
/// <reference path="legacy.d.ts" />
export = jQuery;
其中 types
用于声明对另一个库的依赖,而 path
用于声明对另一个文件的依赖。
如果库的源码本身是由ts写的,那么在使用 tsc 脚本将 ts 编译成 js 的时候,添加 declaration
选项,就可以同步生成 .d.ts
声明文件了。
# 在命令行中添加 --declaration / -d
tsc -d
或者在 tsconfig.json
中添加 declaration
选项:
{
"compilerOptions": {
"module": "commonjs",
"outDir": "lib",
"declaration": true
}
}
使用 tsc 自动生成声明文件时,每个 ts 文件都会对应一个 .d.ts
声明文件,这样的好处是,使用方不仅可以在使用 import foo from 'foo'
导入默认的模块时获得类型提示,还可以在导入一个子模块 import bar from 'foo/lib/bar'
时也获得对应的类型提示。
除了 declaration
选项外,还有几个选项也与自动生成声明文件有关:
declarationDir
设置生成 .d.ts
文件的目录。declarationMap
对每个 .d.ts
文件,都生成对应的 .d.ts.map
(sourcemap) 文件。emitDeclarationOnly
仅生成 .d.ts
文件,不生成 .js
文件。如果声明文件是通过 tsc 自动生成的,那么无需做任何其他配置,只需把编译好的文件也发布到 NPM 上,使用方就可以获取到类型提示了,如果是手动书写的声明文件,那么需要满足以下条件之一,才能被正确的识别:
package.json
中的 types
或 typings
字段指定一个类型声明文件地址index.d.ts
文件。(如果没有指定 types
或 typings
就会在根目录下寻找 index.d.ts
文件)package.json
中 main
字段指定的入口文件),编写一个同名不同后缀的 .d.ts
文件。 (如果没有找到 index.d.ts
文件就会寻找入口文件对应的同名不同后缀的 .d.ts
文件)有的库为了支持导入子模块,比如 import bar from 'foo/lib/bar'
就需要额外编写一个类型声明文件 lib/bar.d.ts
或 lib/bar/index.d.ts
这与自动生成声明文件类似,一个库中同时包含了多个类型声明文件。
与普通的 NPM 包不同,@types 统一由 Definitely Typed 管理的,要将声明文件发布到 @types 下,就需要给 Definitely Typed 创建一个 PR,其中包含了类型声明文件,测试代码,以及 tsconfig.json
等。PR 需要符合它们的规范并通过测试,才能被合并,稍后会被发布到 @types 下。