# 一、 概述
随着前端技术的不断发展,TypeScript(简称:TS)已经在逐步取代JavaScript(简称:JS),尤其在以Vue3使用TS重构后,TS更是成为前端框架编写的主力语言。
在使用TS的时候,最大的一个好处就是可以给JS各种类型约束,使得JS能够完成静态代码分析,推断代码中存在的类型错误或者进行类型提示
TS完成类型推断,需要事先知道变量的类型,如果我们都是用TS书写代码,并且给变量都指定了明确的类型,这时TS可以很好的完成类型推断工作
但是有时,我们不免会引入外部的 JS库,这时TS就对引入的JS文件里变量的具体类型不明确了,为了告诉TS变量的类型,因此就有了.d.ts (d即declare),ts的声明文件。
即 .d.ts 一般是用来描述没有定义类型的 js 的。
笔记
但是也不是说创建了.d.ts文件,里面声明的东西就能生效了,毕竟归根到底也是.ts文件,需要预编译,所以需要在tsconfig.json文件里面的include数组里面添加这个文件。
# 二、 什么是“.d.ts” 文件
基于 Typescript 开发的时候,很麻烦的一个问题就是类型定义。导致在编译的时候,经常会看到一连串的找不到类型的提示。“d.ts”文件用于为 TypeScript 提供有关用 JavaScript 编写的 API 的类型信息。简单讲,就是你可以在 ts 中调用的 js 的声明文件。TS的核心在于静态类型,我们在编写 TS 的时候会定义很多的类型,但是主流的库都是 JS编写的,并不支持类型系统。这个时候你不能用TS重写主流的库,这个时候我们只需要编写仅包含类型注释的 d.ts 文件,然后从您的 TS 代码中,可以在仍然使用纯 JS 库的同时,获得静态类型检查的 TS 优势。在此期间,解决的方式经过了许多的变化,从 DefinitelyTyped (opens new window) 到 typings (opens new window)。最后是 @types (opens new window)。在 Typescript 2.0 之后,推荐使用 @types (opens new window) 方式。
# 2.1 DefinitelyTyped
多数来自 javascript 的库是没有 TypeScript 类型定义的。为了解决这个问题,DefinitelyTyped (opens new window) 被创建出来,它提供了多数流行的脚本库的 TypeScript 定义,你可以使用名为 tsd (opens new window) 的一个工具来管理它。在 Typescript 2.0 中使用 @type 类型定义,这种方式已经不推荐使用。
- 安装tsd工具
npm install tsd -g
# 或
yarn global add tsd
2
3
- 通过工具安装定义库
# 安装jquery定义库
tsd install jquery --save
2
- 配置 如果提供了 --save 参数,它会创建一个名为 tsd.json 的配置文件来保存所管理的类型定义包。
{
"version": "v4",
"repo": "DefinitelyTyped/DefinitelyTyped",
"ref": "master",
"path": "typings",
"bundle": "typings/tsd.d.ts",
"installed": {
"node/node.d.ts": {
"commit": "6834f97fb33561a3ad40695084da2b660efaee29"
}
}
}
2
3
4
5
6
7
8
9
10
11
12
以后,直接使用tsd install
就可以安装定义在配置文件中的所有的包了。
# 2.2 Typings
Typings (opens new window)也是一个用来管理 Typescript 定义的库。这种方式已经不推荐使用。
- 安装typings工具
# Install Typings CLI utility.
npm install typings --global
# 或
yarn global add typings
2
3
4
- 通过工具安装定义库
# 安装jquery定义库
typings install jquery --save
2
安装之后,在你的项目文件夹中会自动创建一个名为 typings 的文件夹来专门保存类型定义的库。 如果这个定义来自 DefinitelyTyped, 则现有加上一个参数 --ambient typings install jquery --ambient --save
- 配置 它自己需要一个配置文件 typings.json,可以用它来管理项目使用到的类型定义,这样,类似于 NPM,我们可以使用 install 命令来自动安装需要的类型定义。typings.json配置如下:
{
"ambientDependencies": {
"jquery": "registry:dt/jquery#1.10.0+20161119044246"
},
"dependencies": {
"jquery": "registry:dt/jquery#1.10.0+20161119044246"
}
}
2
3
4
5
6
7
8
以后,直接使用typings install
就可以安装定义在配置文件中的所有的包了。
# 2.3 @Types
DefinitelyTyped
和# Typings
都需要使用另外一套系统来管理类型定义显然不太方便。在 Typescript 2.0 之后,TypeScript 将会默认的查看 ./node_modules/@types 文件夹,自动从这里来获取模块的类型定义,当然了,你需要独立安装这个类型定义。Microsoft 在 The Future of Declaration Files (opens new window)介绍了 TypeScript 的这个新特性。
默认情况下,所有的 @types 包都会在编译时应用,任意层的 node_modules/@types 都会被使用,进一步说,在 ./node_modules/@types/
, ../node_modules/@types/
, ../../node_modules/@types/
都被应用。如果你的类型定义不在这个文件夹中,可以使用 typesRoot 来配置,只有在 typeRoots 中的包才会被包含,配置如下:
{
"compilerOptions": {
"typeRoots" : ["./typings"]
}
}
2
3
4
5
现在,只有在 ./typings 中的才会应用,而 ./node_modules/@types 中的则不会。 如果配置了 types,则只有列出的包才会包含。
{
"compilerOptions": {
"types" : ["node", "lodash", "express"]
}
}
2
3
4
5
这样将只会包含 ./node_modules/@types/node
, ./node_modules/@types/lodash
和 ./node_modules/@types/express
,其它的则不会被包含进来。如果配置为"types": []
则不会包含任何包。
# 2.4 *.d.ts和@types关系
@types
是npm
的一个分支,用来存放*.d.ts
文件,如果对应的npm
包存放在@types
中,要使用必须下载!如果是自己本地的*.d.ts
申明文件,则和@types没有任何关系!
# 2.5 declare 和 export
.d.ts 文件中的顶级声明必须以 "declare" 或 "export" 修饰符开头。
通过 declare 声明的类型或者变量或者模块,在 include 包含的文件范围内,都可以直接引用而不用去import或者import type相应的变量或者类型。
也就是说,declare 告诉 TS 编译器,你担保这些变量和模块的存在,并声明了响应的类型,编译的时候不需要提示错误。
.d.ts
文件顶级声明 declare 最好不要跟 export 同级使用,不然在其他 ts 引用这个 .d.ts 的内容的时候,就必须手动 import 导入了。- 在
.d.ts
文件里如果顶级声明不用 export 的话,declare和直接写 type、interface 效果是一样的,在其他地方都可以直接引用。
# 三、 编写语法
从类型type
角度分为:基本类型(string、number、boolean、undefined、symbol)其混合 下面我们介绍下 “.d.ts” 的几种声明的写法
# 3.1 全局类型
- 变量
- 函数
- 用interface 声明函数
- class
- 对象
- 混合类型
- 模块化的全局变量
# 3.2 模块化的全局变量
定义全局变量的时候需要引入(别人写的)文件
import { PluginFunc } from 'dayjs'
declare const plugin: PluginFunc
export as namespace plugin; // 可选的全局用法,在UMD上下文中,可在全局作用域下访问 plugin
export = plugin
2
3
4
5
# 3.3 模块化(CommonJS)
通过require的方式引入模块化的代码
// d.ts
declare module "ever" {
export let a: number
export function b(): number
export namespace c{
let c: string
}
}
// 引用
cosnt ever = require('ever)
ever.a = 100
ever.b = function() {
return 100 + 300
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3.4 ES6的模块化方式(import export)
方法和 ES6 的模块化一样。
export declare let a1: 1
export declare let a2: 2
// 或
declare let a1: 1
declare let a2: 2
export { a1,a2 }
2
3
4
5
6
# 3.5 UMD
有一种代码,既可以通过全局变量访问到,也可以通过require的方式访问到。
declare namespace ${
let a:number
export type b=string // 通过export关键字暴露的成员,可以在其它同名的 namespace/class/function 中访问
}
declare module "$" {
export = $
}
2
3
4
5
6
7
8
# 3.6 其他
有时候我们扩展了一些内置对象。给Date的内置对象扩展方法
interface Date {
format(f: string): string
}
2
3
# 四、.d.ts声明文件模板
# 4.1 查看库结构
一个库往往由许多个模块组成,清晰的目录结构十分重要。例如,当我们的库包含如下模块时:
myLib
+---- index.js
+---- foo.js
+---- bar
+---- index.js
+---- baz.js
使用该库时,就有不同的导入:
var a = require("myLib");
var b = require("myLib/foo");
var c = require("myLib/bar");
var d = require("myLib/bar/baz");
2
3
4
则相应的,我们的声明文件应该在如下的目录结构中:
@types/myLib
+---- index.d.ts
+---- foo.d.ts
+---- bar
+---- index.d.ts
+---- baz.d.ts
# 4.2 类型文件测试
如果我们希望自己的库提交到
DefinitelyTyped
供所有人使用,则需要做如下操作:(当然,这种是2.0之前的常见做法,现在之间用@types就好了)- 在
node_modules/@types/[libname]
处创建新的文件夹; - 在该文件夹下创建一个
index.d.ts
文件,并将上面的参考示例复制进去; - 根据我们对模块使用中断的地方,来把
index.d.ts
填充完整; - 当
index.d.ts
文件填充至满意时,下载DefinitelyTyped/DefinitelyTyped (opens new window)并按README
的指导来操作。
- 在
其它情况,例如只需要在自己的项目中使用,则只需做一些简单操作:
在项目根目录下创建一个
[libname].d.ts
文件;在该文件中添加
declare module "[libname]" { }
;将参考示例复制到"{ }"中,并根据模块中断的地方进行填充至满意
declare module "myLib" { export let a: number export function b(): number export namespace c{ let c: string } }
1
2
3
4
5
6
7
# 4.3 拓展插件声明模板
当我们想要拓展库的功能时,可以通过插件声明模板的写法来添加。
该写法下,文件需要包含至少一个顶级的 import
或 export
,即使它只是 export {}
。
以 vue3 官方为例,让 ts 支持 .vue 文件的导入支持。(ts只支持模块的导入导出, 但是有些时候你可能需要引入css/html等文件, 这时候就需要用通配符让ts把他们当做模块)
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// 可以识别vue文件
import X1 from './X1.vue';
export default defineComponent({
components:{X1}
})
2
3
4
5
6
7
8
9
10
11
又比如全局组件的类型声明:
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/vue-next/pull/3399
declare module 'vue' {
export interface GlobalComponents {
TmActionMenu: typeof import('./components/tm-action-menu/tm-action-menu.vue')['default']
}
}
export { }
2
3
4
5
6
7
8
9
10
# 4.4 拓展全局模板
注意:声明模版是用于为库进行声明的,而不是起实际作用的库,真正起作用的是全局修改模块,而不是声明文件本身。
// Type definitions for [~库的名字~] [~版本号,可选~]
// Project: [~工程名~]
// Definitions by: [~汝名~] <[~主页链接~]>
/*~ 这是个全局修改模块的模板,本文件应该被重命名为 index.d.ts
*~ 并置于与模块同名的文件夹下,
*~ 例如,模块名为 "super-greeter",
*~ 则本文件路径为 'super-greeter/index.d.ts'
*/
/*~ 注意,如果你的全局修改模块可以直接调用或者用作构造函数,
*~ 则应在此结合模块函数或者模块类的模板文件。
*/
declare global {
/*~ 此处声明的成员将被置于全局global命名空间中,
*~ 或用于增强该命名空间中已有的成员。
*/
interface String {
fancyFormat(opts: StringFormatOptions): string;
}
}
/*~ 如果你的模块需要导出值或者类型,照常写就ok */
export interface StringFormatOptions {
fancinessLevel: number;
}
/*~ 例如声明一个方法 */
export function doSomething(): void;
/*~ 如果你的模块啥也不导出,则写下面这句话,否则,删掉这句话 */
export {};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 五、案例
/** 作为函数使用 */
declare function People(w: number): number
declare function People(w: string): number
declare class People {
/** 构造函数 */
constructor(name: string, age: number)
constructor(id: number)
// 实例属性和实例方法
name: string
age: number
getName(): string
getAge(): number
/** 作为对象,调用对象上的方法或者变量 */
static staticA(): number
static aaa: string
}
/** 作为对象,调用对象上的方法或者变量 */
declare namespace People {
export var abc: number
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24