TypeScript 类型声明
全局声明
前面提到了有导入、导出语句存在的文件被 TS 视为一个模块,模块内的类型有自己的作用域,其他文件只能导入后使用。而没有导入、导出语句的文件则被 TS 视为全局脚本文件,全局脚本文件被 TS 编译后,其中的类型便可以在 TS 编译范围内的其他文件中直接使用。
我们可以创建一个示例项目查看:
# 创建 ts-test 项目文件夹
mkdir ts-test
# 进入文件夹
cd ts-test
# 创建 package.json
npm init -y
# 创建 tsconfig.json(需要全局安装 typescript 库)
tsc --init
2
3
4
5
6
7
8
创建 ts-test
项目后,使用编辑器打开,分别创建 a.ts
和 b.ts
文件作为示例。在 a 文件中写入一些类型:
// a.ts
type A = number;
interface B {
b: string;
}
class C {
c = true;
}
2
3
4
5
6
7
8
9
10
然后在 b 文件中使用这些类型:
// b.ts
let a: A = 1;
let b: B = { b: "b" };
let c: C = { c: true };
2
3
4
5
6
可以看到 b 文件中虽然没有引入 A、B、C 类型,但是依然没有报错。这是因为 tsconfig.json 中include
编译选项的默认值包含了所有文件,所以 a 文件会被 ts 编译。而 a 文件没有导入导出语句,会被 ts 识别为一个全局声明文件,所以内部的类型可以直接在其他编译范围内的文件中使用。
前文提到了同名 interface、class 会进行类型合并,所以在全局声明文件中,也可以利用这个特性来拓展类型。例如可以往全局的Window
类型中添加定义:
// a.ts
interface Window {
d: "d";
}
2
3
4
// b.ts
let d = window.d; // 不报错,此时变量 d 的类型为 “d”
2
全局声明文件的作用就是用来定义全局可用的类型,此时如果将 a 文件中的类型定义都注释掉就可以看到 b 文件中全部类型使用都报错了。
内置声明文件
安装 TS 时,会同时安装一些内置类型声明文件,主要是内置的全局对象(JS 和浏览器 API 等)类型声明。
紫萼内置声明文件位于 TS 安装目录的 lib
文件夹中,大概有几十个,例如:
- lib.d.ts
- lib.dom.ts
- lib.es5.d.ts
- lib.es2017.d.ts
编译器会自动根据编译目标 target
的值,加载对应的内置声明文件,所以不需要特别的配置。但是,可以使用编译选项 lib
,指定加载哪些内置声明文件:
{
"compilerOptions": {
"lib": ["dom", "es2021"]
}
}
2
3
4
5
还有一个编译选项 noLib
会禁止加载任何内置声明文件。
declare 关键字
declare
关键字用来告诉编译器,某个值是存在的,可以根据 declare 给出的类型在当前文件中使用。
declare 关键字的重要特点是,它只是通知编译器某个类型是存在的,不用给出具体实现。比如,只描述函数的类型,不给出函数的实现,如果不使用 declare,这是做不到的。
declare 关键字可以描述以下类型:
1. declare variable
例如你知道某个脚本定义了全局变量 x,但是 TS 不知道,所以直接在当前文件中使用会报错:
x = 123; // 报错
此时可以用 declare 命令给出它的类型,就不会报错了。如果 declare 没有给出变量的具体类型,那么会被当作 any 类型:
declare let x: number;
// declare let x; // 此时 x 为 any 类型
x = 123; // 正确
2
3
4
注意,declare 关键字只用来给出类型描述,是纯的类型代码,不允许设置变量的初始值。
2. declare function
declare 关键字可以给出外部函数的类型描述:
declare function sayHello(name: string): void;
sayHello("张三");
2
3
3. declare class
declare 给出 class 类型描述的写法如下:
declare class Animal {
constructor(name: string);
eat(): void;
sleep(): void;
}
2
3
4
5
4. declare module、declare namespace
如果想将变量、函数、类组织在一起,可以用 declare module
或 declare namespace
。二者代码块中可以添加 export
关键字,也可以不加:
declare namespace myLib {
// export 也可以不加
export function makeGreeting(s: string): string;
export let numberOfGreetings: number;
}
// 或者
// declare module myLib {
// function makeGreeting(s: string): string;
// let numberOfGreetings: number;
// }
let result = myLib.makeGreeting("你好");
let count = myLib.numberOfGreetings;
2
3
4
5
6
7
8
9
10
11
12
13
declare 关键字的另一个用途,是为外部模块添加属性和方法时,给出新增部分的类型描述:
import { Foo as Bar } from "./a";
declare module "./a" {
interface Foo {
custom: string;
}
}
2
3
4
5
6
7
上面示例中,从模块 moduleA
导入了类型 Foo
,它是一个接口(interface),并将其重命名为 Bar
,然后用 declare
关键字为 Foo
增加一个属性 custom
。这里需要注意的是,虽然接口 Foo
改名为 Bar
,但是扩充类型时,还是扩充原始的接口 Foo
,因为同名 interface 会自动合并类型声明。
WARNING
使用这种语法进行模块的类型扩展时,有两点需要注意:
declare module NAME
语法里面的模块名NAME
,跟 import 和 export 的模块名规则是一样的,且必须跟当前文件加载该模块的语句写法(上例import { A } from './a'
)保持一致。- 不能创建新的顶层类型。也就是说,只能对
a.ts
模块中已经存在的类型进行扩展,不允许增加新的顶层类型,比如新定义一个接口。 - 不能对默认的 default 接口进行扩展,只能对 export 命令输出的命名接口进行扩充。这是因为在进行类型扩展时,需要依赖输出的接口名。
某些第三方模块,原始作者没有提供接口类型,这时可以在自己的脚本顶部加上下面一行命令:declare module "模块名";
。此时该模块内部的接口都将视为 any
类型。
declare module
描述的模块名还可以使用通配符:
declare module "my-plugin-*" {
interface PluginOptions {
enabled: boolean;
priority: number;
}
function initialize(options: PluginOptions): void;
export = initialize;
}
2
3
4
5
6
7
8
9
这里的 export = initialize
是描述模块内的默认导出,表示默认导入这个模块时得到的就是 initialize
的值,这是 CommonJS 中的默认导出语法,对应的还可以使用 export default initialize;
。
5. declare global
如果要为 JS 原生对象添加属性和方法,可以使用 declare global
语法:
export {};
declare global {
// 扩展浏览器全局对象
interface Window {
myAppConfig: object;
}
}
const config = window.myAppConfig;
2
3
4
5
6
7
8
9
10
示例第一行的空导出语句 export {}
,作用是强制编译器将这个脚本当作模块处理。这是因为 declare global
必须用在模块里面。declare global
只能扩充现有对象的类型描述,不能增加新的顶层类型。
6. declare enum
declare enum E1 {
A,
B,
}
declare const enum E2 {
A = 0,
B = 1,
}
2
3
4
5
6
7
8
9
7. declare module 用于类型声明文件
.d.ts 类型声明文件
单独使用的模块,一般会提供一个单独的类型声明文件(declaration file),类型声明文件中只有类型代码,没有具体实现。所以文件名一般为 name.d.ts
的形式。
例如有一个 a.ts
文件如下:
export const num = 123;
function foo(flag: boolean) {
return !flag;
}
export { foo };
export default "abc";
2
3
4
5
6
7
8
9
使用 tsc --declartion
命令生成的 a.d.ts
文件就是:
export declare const num = 123;
declare function foo(flag: boolean): boolean;
export { foo };
declare const _default: "abc";
export default _default;
2
3
4
5
6
7
上面用到了三种导出语法,可以看到对应的类型声明语法只是添加了 declare 关键字声明值后再导出。
除了 tsc 命令生成 .d.ts 文件外,还可以自己编写声明文件。例如上面的全局声明就可以写在没有导入、导出语句的 .d.ts 文件内。也可以导出一系列类型,供其他文件导入后使用,例如:
// types.d.ts
export interface Character {
catchphrase?: string;
name: string;
}
// index.ts
import { Character } from "./types";
export const character: Character = {
catchphrase: "Yee-haw!",
name: "Sandy Cheeks",
};
2
3
4
5
6
7
8
9
10
11
12
13
声明文件来源
声明文件主要有三种来源:
- 自动生成
上面提到了加入 declaration
编译选项后,ts 文件生成 js 文件的同时会自动生成对应的类型声明文件。
当其他文件引用 js 文件时,如果 ts 编译器查找到了目录下有同名的 .d.ts 文件,那就会使用这个声明文件中的类型。
- 内置声明文件
前面提到了 TS 中有一些内置类型声明文件,TS 编译器自动根据编译目标 target
值,和编译选项 lib
值来确定需要加载的内置声明文件
编译选项 noLib
会禁止加载任何内置声明文件。
- 外部类型声明文件
如果库自带了类型声明文件,例如入口文件的同名 .d.ts 文件时,就会使用这个自带类型声明。
如果库没有提供类型声明文件,往往会有社区提供。这些文件会作为一个单独的库,发布到 npm 的 @types
名称空间下。例如安装 jQuery 的类型声明文件: npm install @types/jquery -D
。
此时 @types/jquery
就会安装到 node_modules 中,里面的 index.d.ts
就是 jQuery 的声明文件。如果类型声明文件名不是 index,那就需要在 package.json 中指定 types
或 typeing
字段类指定类型声明文件路径。
TS 会自动加载 node_modules/@types
目录下的模块,也可以使用 typeRoots
改变自动加载的目录:
{
"compilerOptions": {
"typeRoots": ["./typings", "./vendor/types"]
}
}
2
3
4
5
如上设置后,TS 就不再从 node_modules/@types
目录中加载,而是去 typings
和 vendor/types
目录下加载类型模块。
默认情况下,会加载 typeRoots 指定目录中的所有模块。可以使用 types
编译选项指定需要加载哪些模块:
{
"compilerOptions": {
"types": ["jquery"]
}
}
2
3
4
5
如上指定后,TS 就只会加载 typeRoots
目录下的 jquery 模块对应类型声明文件。
当没有第三方库类型声明文件时,可以自己定义该库的类型描述,比如使用 jQuery 脚本可以写成这样:
// jquery.d.ts
declare var $: any;
// 或者
declare type JQuery = any;
declare var $: JQuery;
2
3
4
5
6
这是一个全局声明文件,定义了 $
变量的类型为 any
,之后就可以在其他地方使用 jquery 而不报错。
也可以采用下面的写法,来定义整个模块的类型:
// jquery.d.ts
declare module "jquery" {
// 具体接口定义
}
// 或者将整个模块设为 any 类型
declare module "jquery";
2
3
4
5
6
7
可以在模块内部定义具体的接口类型,也可以直接将模块定义为 any 类型。
模块发布
一个模块如果包含自己的类型声明文件,可以在 package.json 文件中添加一个 types
或 typings
字段,指明类型声明文件的位置:
{
"name": "awesome",
"version": "1.0.0",
"main": "./lib/main.js",
"types": "./lib/main.d.ts"
}
2
3
4
5
6
如果类型声明文件名为 index.d.ts
,且在项目根目录中;或者类型声明文件路径与名称都和代码文件相同。那么可以不另外注明,但根据最佳实践,还是建议添加 types 字段。
三斜杠命令
如果类型声明文件的内容非常多,可以拆分成多个文件,然后入口文件使用三斜杠命令,加载其他拆分后的文件。例如入口文件是 main.d.ts
,里面的接口定义在 interfaces.d.ts
,函数定义在 functions.d.ts
。那么,main.d.ts
里面可以用三斜杠命令,加载后面两个文件。
/// <reference path="./interfaces.d.ts" />
/// <reference path="./functions.d.ts" />
2
三斜杠命令(///
)是一个 TS 编译器命令,用来指定编译器行为。它只能用在文件的头部(三斜线命令之前只允许使用单行注释、多行注释和其他三斜线命令),如果用在其他地方,会被当作普通的注释。
除了拆分类型声明文件,三斜杠命令也可以用于普通脚本加载类型声明文件。有个重要作用是在全局声明文件中加载其他类型声明,因为全局声明文件不能使用导入导出,但可以使用三斜线命令加载。
三斜杠命令主要包含三个参数,代表三种不同的命令:
/// <reference path="" />
/// <reference path="" />
告诉编译器需要包括的文件的路径(或者库名,类似 import 导入),需要注意:
path
参数必须指向一个存在的文件,若文件不存在会报错。path
参数不允许指向当前文件。
默认情况下,每个三斜杠命令引入的脚本,都会编译成单独的 JS 文件。如果希望编译后只产出一个合并文件,可以使用编译选项 outFile
。但是,outFile
编译选项不支持合并 CommonJS 模块和 ES 模块,只有当编译参数 module 的值设为 None
、System
或 AMD
时,才能编译成一个文件。
如果打开了编译参数 noResolve
,则忽略三斜杠指令。将其当作一般的注释,原样保留在编译产物中。
/// <reference types="" />
/// <reference types="" />
告诉编译器当前脚本依赖某个类型库,通常安装在 node_modules/@types
中,types 参数指定类型库的名称。
注意,这个命令只在你自己手写类型声明文件(.d.ts 文件)时,才有必要用到。如果是普通的 .ts 脚本,可以使用 tsconfig.json 文件的 types 属性指定依赖的类型库。
/// <reference lib="" />
/// <reference lib="" />
命令允许脚本文件显式包含内置 lib 库,等同于在 tsconfig.json 中使用 lib 属性指定库。
lib 参数指定的就是内置 lib 库的描述部分(去除 .d.ts 的部分)