**类型声明(Type Declaration)或者类型定义(Type Definition)**文件是一个以.d.ts
作为文件后缀名的TypeScript文件。它们与普通.ts
文件有什么区别呢?有什么特征呢?接下来,一起深入了解下。
.d.ts
文件 #类型声明文件的后缀名为.d.ts
,文件中只包含与类型相关的代码,不包含逻辑代码,它们的作用旨在为开发者提供类型信息,所以它们只在开发阶段起作用。
在TypeScript编译文章中介绍过,typescript编译后会将类型信息移除,类型信息仅在开发阶段起作用。
类型声明,即类型的声明,例如interface
、function
和class
,下面是一个interface
声明。
// Person接口类型声明
interface Person {
name: string;
age: number;
}
// 使用Person接口
const xm: Person = {
name: "XiaoMing",
age: 8
};
**全局声明(global declaration)**在任何TypeScript项目或者TypeScript代码片段中都会起作用的。例如当你写下const p = new Promise();
时,TypeScript编译器不会去编译你的代码,因为const p = new Promise();
语法错误了,如果你使用的是vscode IDE,那么vscode向下图中那样提示你(个人写代码,习惯用白色背景,vscode默认的背景色是黑色,虽然主题色不同,但是提示是相同的)。
vscode就是根据全局声明检测到你的语法错误的。每个TypeScript程序都可以使用全局声明,无需显示的用import
语句导入。
从IDE的提示中,可以看到Promise
类型是定义在lib.es2015.promise.d.ts
文件中的,由TypeScript提供,TypeScript提供的很多这样的声明文件,被称作标准库(这些声明文件会随TypeScript一起安装)。
使用
lib
编译选项,你可以控制哪些标准库被引入到当前项目。也可以设置noLib
为true
,这样会在项目中禁用所有标准库。更多内容请点击Compilation。
为了手动的去加载一些全局声明文件,我们需要告诉TypeScript编译器声明文件的位置。可以通过typeRoots
和types
编译选项设置,关于这些内容,我也在Compilation里面讲解过。
接下来,动手实践。创建以下文件结构。
project/
types/
common/
main.d.ts
package.json
program.ts
tsconfig.json
在我们的项目中,我们需要一些公共类型,在每个ts文件中都可以使用这些公共类型,所以我打算在project/types/common/main.d.ts
中存放这些公共类型声明,使用typeRoots
告诉TypeScript声明文件的位置。TypeScript编译器会根据Node模块定位策略导入typeRoots
指定的目录下的声明文件,如果没有提供typeRoots
选项,默认会导入node_modules/@types
文件夹中的声明文件。
node_modules/@types
是typeRoots
的默认值,node_modules/@types
的目的仅仅是为了提供公共包的类型声明,目录下的每个声明模块的根目录下都应该包含一个index.d.ts
文件,用来作为typescirpt入口声明文件,如果没有index.d.ts
文件,它应该有一个package.json
文件,package.json
文件需要指出入口声明文件的位置。
现在我需要把project/types/common/main.d.ts
文件,作为入口声明文件,所以在project/types/common/package.json
文件中需要添加以下内容。
{
"name": "common",
"version": "1.0.0",
"typings": "main.d.ts"
}
typings
用于指示入口声明文件的位置。
接下来,配置typeRoots
,告诉TypeScript自定义全局声明的位置,修改tsconfig.json
文件内容如下。
{
"files": [
"./program.ts"
],
"compilerOptions": {
"typeRoots": [
"./types"
]
}
}
在main.d.ts
文件中添加声明信息。
interface Person {
firstName: string;
lastName: string;
age: number;
}
有了声明信息,可以在program.ts
中使用该类型了。
正如图片中显示的那样,定义了Person
类型的ross
变量,因为赋值与类型不兼容,所以IDE报错,IDE还给出了声明文件的位置。修改program.ts文件如下,修复错误。
const ross: Person = {
firstName: "Ross",
lastName: "Geller",
age: 20
};
在上面的例子中,TypeScript仅仅知道main.d.ts
声明文件,假如项目中包含几千个声明信息,将这些声明全部放到main.d.ts
文件中,会使项目变得非常糟糕,不好维护。因此,需要将声明模块化。
在命名空间教程中,我介绍过可以使用<reference />
指令,在编译阶段添加一些文件。当然我们也可以在声明文件中使用该指令。首先project/types/common/
文件夹下添加functions.d.ts
和interfaces.d.ts
文件。
interfaces.d.ts
提供common
包中所有的接口类型信息,functions.d.ts
提供common
包中所有的函数类型信息。并在main.d.ts
文件中,使用<reference />
指令引入它们。
// types/common/main.d.ts
/// <reference path="./interfaces.d.ts" />
/// <reference path="./functions.d.ts" />
-----------------------------------------------
// types/common/interfaces.d.ts
interface Person {
firstName: string;
lastName: string;
age: number;
}
------------------------------------------------
// types/common/functions.d.ts
type GetFullName = ( p: Person ) => string;
有一点值得注意的是,我们可以在functions.d.ts
文件中,使用interfaces.d.ts
文件中定义的Person
接口,这是因为使用<reference />
指定,指定的文件可以共享类型声明。
修改program.ts
文件使用定义的类型。
const ross: Person = {
firstName: "Ross",
lastName: "Geller",
age: 20
};
const getPersonName: GetFullName = (p: Person) => {
return `${p.firstName} ${p.lastName}`;
};
console.log(getPersonName(ross));
目前,已经知道了如何创建自定义全局声明文件。某些情况下,我们需要安装第三方提供的npm声明文件包。
在Compilation文章中我提到过DefinitelyTyped社区,它们为我们提供了很多第三方npm声明文件包,例如@types/lodash
和@types/node
等等。你可以使用npm安装@types
下的声明包。
npm install -D @types/node
这些包会被下载到node_modules/@types
文件夹下,这也是为什么typeRoots
的默认值为node_modules/@types
。如果你想在默认值的基础上额外添加一个声明文件夹目录,需要给typeRoots
指定两个值。
{
"files": [
"./program.ts"
],
"compilerOptions": {
"typeRoots": [
"./node_modules/@types",
"./types"
]
}
}
某些值,只存在于运行时。例如window
对象,它只存在于浏览器环境中,同样的还有node中的global
。
如果当你在TypeScript程序中,使用window
或者glbal
时,typescirpt编译器报错了cannot find name 'window'
。这是正常的,因为没有定义window
变量。所以,如果没有定义,我们如何使用window
对象呢?
我们需要使用declare
关键字告诉TypeScript编译器,window
变量存在,不要再报错了。
declare var window: any;
这句代码,被称为环境声明(ambient declaration),其中window
被称为环境值(ambient value)。有了这个声明,TypeScript会认为这个any
类型的window
变量已经定义了,所以在任何地方使用window
变量都不会报错了。
你也可以为环境值提供指定的类型,例如declare var window: Person;
接下来,在main.d.ts
文件中添加一个环境值XiaoMing
。
/// <reference path="./interfaces.d.ts" />
/// <reference path="./functions.d.ts" />
declare var XiaoMing: Person;
然后在program.ts
中使用它。
console.log(XiaoMing.age);
如果没有写错的话,TypeScript不会报错。
在前面的例子中,我们提供了types/common
声明包,定义了全局Person
接口类型,全局的接口类型有一个弊端,就是很容易被其他包里的声明文件类型覆盖(类型冲突)。
为了避免这个问题,最好的解决方式是将类型定义在命名空间里。在namespace文章中,介绍了namespace的用法,我们可以在命名空间里定义类型声明。
修改声明文件内容如下。
// types/common/main.d.ts
/// <reference path="./interfaces.d.ts" />
/// <reference path="./functions.d.ts" />
--------------------------------------------------------------------
// types/common/interfaces.d.ts
declare namespace common {
interface Person {
firstName: string;
lastName: string;
age: number;
}
}
--------------------------------------------------------------------
// types/common/functions.d.ts
declare namespace common {
type GetFullName = ( p: Person ) => string;
}
这里定义的命名空间类型与namespace文章中有点不一样,区别在于使用了declare关键字。因为namespace
是一个值(object),在声明文件.d.ts
文件中应该将namespace
定义成环境值,这样我们可以在任何ts
文件中,通过common
命名空间使用common.<type>
类型,例如common.Person
。
再次提醒,在声明文件中只允许包含声明信息,因为命名空间是一个object,所以不能在声明文件中定义它,但是我们可以将命名空间定义成环境值。
并且,当命名空间被定义在声明文件中,export
关键字就不需要了,命名空间内部的所有类型,都被隐式export。另外,你不能从命名空间中导出一个值,只允许导出类型。
当typescript预见多个同名接口类型,它会自动合并这些接口类型成为一个声明。
namespace也有类似的行为,当声明一个已经存在的命名空间时,typescript会自动扩展已经存在的命名空间。所以当接口和命名空间在全局环境中,可以重新声明它们来达到扩展它们的目的。
更新声明文件。
// types/common/main.d.ts
/// <reference path="./interfaces.d.ts" />
/// <reference path="./functions.d.ts" />
--------------------------------------------------------
// types/common/interfaces.d.ts
interface Person {
firstName: string;
lastName: string;
age: number;
}
--------------------------------------------------------
// types/common/functions.d.ts
declare namespace common {
type GetFullName = ( p: Person ) => string;
}
更新program.ts
。
interface Person {
email: string;
}
interface Number {
isEven(): boolean;
}
const ross: Person = {
firstName: "Ross",
lastName: "Geller",
age: 29,
email: "ross@geller.com"
};
const isAgeEven = ross.age.isEven();
console.log("isAgeEven", isAgeEven);
此时typescript并不会报错。在program.ts
中扩展了Person
接口和Number
(内置类型)接口。
但是,这并不适用于模块系统,值和类型只能在script文件中共享,script文件就是不包含import
和export
语句的文件。
可以在program.ts
文件最后面添加一行代码export {}
。此时typescript应该会给出以下错误信息。
Type '{ firstName: string; lastName: string; age: number; email: string; }' is not assignable to type 'Person'.
Object literal may only specify known properties, and 'firstName' does not exist in type 'Person'.ts(2322)
Property 'age' does not exist on type 'Person'.ts(2339)
因为,添加export {}
代码,将program.ts
文件转换成了模块(module),这种情况下,typescript不会扩展全局环境中的类型,而是在当前模块内重新声明该类型。
那么如何在模块中,扩展全局环境中的类型呢?TypeScript提供了declare global
环境声明语句,使用它,可以在模块内给全局作用域添加声明。
更新program.ts
,使用declare global
语句。
declare global {
// 扩展全局Person接口
interface Person {
email: string;
}
}
declare global {
// 扩展全局Number接口
interface Number {
isEven(): boolean;
}
var version: string;
}
const ross: Person = {
firstName: 'Ross',
lastName: 'Geller',
age: 29,
email: 'ross@geller.com'
}
const isAgeEven = ross.age.isEven();
console.log( 'isAgeEven', isAgeEven );
export {}
这次typescript不会报错,并且成功在module内扩展了全局类型。
(完)