TypeScript基础篇 --- 声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

新语法索引
  • declare var 声明全局变量
  • declare function 申明全局函数
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interfacetype 声明全局类型
  • export 导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES5默认导出
  • export = commonjs导出模块
  • declare global 扩展全局变量
  • declare module 扩展模块
  • /// <reference> 三斜杠指令
什么是声明语句

假设我们想使用第三方库jQuery,一种常见的方式是在Html中通过<script>标签映入jQuery,然后就可以使用全局变量$jQuery了。
我们通过这样取一个idfoo的元素:

    $('#foo') //找不到名称 "$"。是否需要安装 jQuery 的类型定义? 请尝试 `npm i @types/jquery`。t

但是在ts中,编译器并不知道$是什么东西,这时我们需要使用declare var来定义它的类型:

    declare var $:(select:string)=>any;
    $('#foo') 

上例中,declare var并没有真的定义一个变量,只是定义了全局变量$的类型,仅仅会用域编译时的检查,在编译结果中会被删除。

    $('#foo') 
什么是声明文件

通常我们会把声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件:

    // src/jQuery.d.ts
    declare var jQuery: (selector: string) => any;
// src/index.ts
jQuery('#foo');
第三方申明文件

推荐的是使用 @types 统一管理第三方库的声明文件。
@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:

    npm install @types/jquery --save-dev

可以在这个页面搜索你需要的声明文件。

书写声明文件

当一个第三方库没有提供申明文件时,我们就需要自己书写声明文件了。前面只介绍了嘴贱的声明文件内容,而真正的书写一个声明文件并不是一个简单事情,以下会详细介绍如何书写声明文件。
在不同场景下,声明文件的内容和使用方式会有所区别。
库的使用常见主要有一下集中:

  • 全局变量:通过<script>标签引入的第三方库,注入全局变量。
  • npm包:通过import foo from 'foo' 导入,符合es6模块规范
  • UMD库:既可以通过<script>标签映入,又可以通过import导入
  • 直接扩展全局变量:通过<script>标签引入后,改变一个全局变量的结构
  • 在npm包或者UMD库中扩展全局变量:引用npm包或者UMD库后,改变一个全局变量的结构
  • 模块插件:通过<script>import导入后,改变另一个模块的结构
全局变量

全局变量是最简单的一种场景,之间举得例子就是通过<script>标签引入的jQuery,注入的全局变量$jQuery
使用全局变量的声明文件时候,如果是以npm install @types/xxx --save-dev安装的,则不需要任何配置。如果是将声明文件直接放于当前项目重,则建议和其他源码一起放到scr目录下(或者对应的源码目录下)。

├── src
|  ├── index.ts
|  └── jQuery.d.ts
└── tsconfig.json

如果没有生效,可以检查下tsconfig.json中的fileincludeexclue配置,确保其包含了.d.ts文件。
全局变量的声明文件主要有以下几种语法

  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性)全局对象
  • interfacetype 声明全局类型

declare var

在所有的声明语句中,declare var是最简单的,如之前所学,它能够用来定义一个全局的变量的类型。与其类似,还有declare letdeclare const,使用let与使用var没有什么区别:

    // src/jquery.d.ts
    declare let Query:(selector:string)=>any;
    Query('#foo')
    // 可以修改
    Query = function(select){
        return document.querySelector(select)
    }

而当我们使用const定义时,表示此时的全局变量是一个常量,不允许再去修改他的值了:

    declare const Query:(selector:string)=>any;
    Query('#foo')
    // 不可以修改
    Query = function(select){
        return document.querySelector(select)
    } // 无法分配到 "Query" ,因为它是常数。

一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用const而不是varlet
需要注意的是,声明语句中只能定义类型,切勿在声明语句中定义具体的实现:

    declare const jQuery = function (select) {
        return document.querySelector(select)
    }//(local function)(select: any): any 环境上下文中的 "const" 初始化表达式必须为字符串、数字文本或文本枚举引用。

declare function
declare function用来定义全局函数的类型。

    declare const Query:(selector:string)=>any;

在函数类型的声明语句中,函数重载也是支持的:

    declare function jQuery(selector: string): any;
    declare function jQuery(domReadyCallback: () => any): any;

declare class
当全局变量是一类的时候,我们用declare class来定义他的类型:

    declare class Animal {
        name:string
        constructor(name : string) 
        sayHi():string
    }

同样的,declare class语句也只能用来定义类型,不能用来定义具体的实现。

declare enum
使用declare enum定义的枚举类型也称作外部枚举。

    declare enum Directions {
        Up,
        Down,
        Left,
        Right
    }

使用

    let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

其中,Directions是第三方库定义好的全局变量。
与其他全局变量的类型声明一致,declare enum仅用来定义类型,而不是具体的值。
和正常的类型一样,仅仅用于编译时的检查,声明文件的内容在编译结果中会被删除。

declare namespace
namespaceTypeScript 早起为了姐姐模块化而创造的关键字,中文成为命名空间。
由于历史遗留原因,在早起还没有ES6的时候,ts提供了一种模块方案,使用module关键字表示内部模块。但由于后来es6也使用了module关键字,ts为了兼容es6,使用namespace替代了自己的module,更名为命名空间。
随着es6的广泛应用,现在已经不建议使用ts中的namespace,而推荐使用ES6的模块化方案。
namespace被淘汰了,但是在声明文件中,declare namespace还是比较常用的,他用来表示全局变量是一个对象,包含很多子属性。

    declare namespace jQuery{
        function ajax(url:string,setting?:any):void;
    }

声明了一个全局变量jQuery,提供一个jQuery.ajax方法可以调用。
在内部我们使用function ajax来声明函数,而不是declare function,类似的,我们也可以使用constvarletenum

嵌套的命名空间

如果对象拥有深层的层级,则需要用嵌套的namespace来声明深层的属性的类型:

    declare namespace jQuery{
        function ajax(url:string,setting?:any):void;
        namespace fn{
            function extend(object:any):void
        }
    }

假如jQuery下仅有fn这一个属性(没有ajax等其他属性或方法),则可以不需要嵌套namespace:

    declare namespace jQuery.fn {
        function extend(object: any): void;
    }

interface 和 type
除了全局变量之外,可能还有一些类型我们也希望能暴露出来,在声明文件中,我们可以直接使用interfacetype来声明一个全局的接口或者类型:

    interface AjaxSettings {
        method?:'GET' | 'POST',
        data?: any;
    } 
    declare namespace jQuery {
        function ajax(url: string, settings?: AjaxSettings): void;
    }

这样话的,在其他的文件也可以使用这个接口或类型了。

防止命名冲突

暴露在最外层的interfacetype会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或者全局类型的数量。故最好将他们放在namespace中。

    declare namespace myInterface {
        interface a {
            name: string
        }
    }
声明合并

假如jQuery既是一个函数,可以直接被调用,又是一个对象,那么我们可以组合多种声明语句,他们会不冲突的合并起来。

    declare namespace jQuery {
        function ajax(url: string, settings?: AjaxSettings): void;
    }
    declare namespace jQuery {
        
    }
npm 包

一般我们通过(import foo from 'foo)导入一个npm包,这是符合ES6模块规范的。
在我们尝试给一个npm包创建声明文件之前,需要先看看它的声明文件是否已经存在,一般来说,npm包的声明文件可能存于两个地方:

  1. 与该npm包绑定在一起。判断依据是package.json中有types字段,或者有一个index.d.ts声明文件。这种模式不需要额外安装其他包,是最为退件的,所以以后我们自己创建npm包的时候,最好也将声明文件与npm包绑定在一起。
  2. 发布到@types里。我们只需要尝试安装一下对应的@types包就知道是否存在改声明文件npm install @types/foo --save-dev。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 @types 里了。

假如上面两种方式都没有找到对应的声明文件,那么我们就需要自己为他写声明文件了。由于是通过import语句导入的模块,所以声明文件存放的文职也有所约束,一般有两种方案:

  1. 创建一个 node_modules/@types/foo/index.d.ts 文件,存放 foo 模块的声明文件。这种方式不需要额外的配置,但是 node_modules 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。
  2. 创建一个 types 目录,专门用来管理自己写的声明文件,将 foo 的声明文件放到 types/foo/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 paths 和 baseUrl 字段。
    /path/to/project
    ├── src
    |  └── index.ts
    ├── types
    |  └── foo
    |     └── index.d.ts
    └── tsconfig.json

tsconfig.json 内容:

    {
        "compilerOptions": {
            "module": "commonjs",
            "baseUrl": "./",
            "paths": {
                "*": ["types/*"]
            }
        }
    }

如此配置之后,通过 import 导入 foo 的时候,也会去 types 目录下寻找对应的模块的声明文件了。
不管采用了以上两种方式中的哪一种,我们都强烈建议大家将书写好的声明文件(通过给第三方发pull request,或者直接提交到@types)发布到开源社区中。
npm 包的声明文件主要有以下几种语法:

  • export导出变量
  • export namespace 导出含有(子属性)的对象
  • export = commonjs导出模块

export

npm包的声明文件与全局变量的声明文件有很大区别。在npm包的声明文件中,使用declare不再会声明一个全局保留,而只会声明一个局部变量。只有在声明文件中使用export导出,然后在使用方import导入后,才会应用到这些类型声明。
export的语法与普通的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;
    }

对应的导入和使用模块应该是这样:

    import { name, getName, Animal, Directions, Options } from 'foo';
混用declareexport

我们也可以使用declare先声明多个变量,最后再用export一次性导出。

    declare const name: string;
    declare function getName(): string;
    declare class Animal {
        constructor(name: string);
        sayHi(): string;
    }
    declare enum Directions {
        Up,
        Down,
        Left,
        Right
    }
    interface Options {
        data: any;
    }

    export { name, getName, Animal, Directions, Options };

注意,与全局变量的声明文件类似,interface 前是不需要 declare 的。

export namespace
declare namespace类似,declare namespace用来导出一个拥有子属性的对象:

    export namespace foo {
        const name: string;
        namespace bar {
            function baz(): string;
        }
    }

export default
在ES6模块系统中,使用export default可以导出一个默认值,使用方可以用 import foo from ""而不是import {foo} from ""

    export default function foo(): string;

注意,只有functionclassinterface可以直接默认导出,其他的变量需要先定义出来,再默认导出。

export =
commonjs规范中,我们用一下方式来导出一个模块:

// 整体导出
module.exports = foo;
// 单个导出
export.bar = bar

在ts中,针对这种模块导出,有多重方式可以导入。
require:

// 整体导入
const foo = require('foo')
// 单个导入
const bar = require('foo).bar

import:

// 整体导入
import * as foo from 'foo';
// 单个导入
import { bar } from 'foo';

import require:

// 整体导入
import foo = require('foo');
// 单个导入
import bar = foo.bar;

对于这种使用commonjs规范的库,加入要为他写类型声明文件的话,就需要使用到export=:

export = foo;
declare function foo():string;
declare namespace foo {
    const bar:number;
}

需要注意的是,上例中使用了export=之后,就不能再单个导出export {bar}了,所以我们通过声明合并,使用declare namesace foobar合并到foo里。
准确的讲,export =不仅可以用在声明文件中,也可以用在普通的ts文件中。实际上,import ... requireexport =都是ts为了兼容AMD和commonjs规范创立的新语法,并不常用也不推荐使用。
由于很多第三方库是commonjs规范的,所以声明文件也就不得不使用到这种语法了。但是如果可以,更加退件使用ES6标准的export defaultexport

UMD库

即可以通过<script>标签引入,有可以通过import导入的库,称为UMD库。相比于npm库,我们需要额外声明一个全局变量,为了实现这种方式,ts提供了一个新语法export as namesapce

export as namespace
一般使用export as namesace时,都是现有了npm包的声明文件,再基于它添加一条export as namespace语句,即可将声明变成全局变量:

export as namesace foo;
export = foo;
declare function foo():string
declare namespace foo {
    const bat : number
}

当然他也可以和export default一起使用:

export as namespace foo;
export default foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}
直接扩展全局变量

有的第三方库扩展了一个全局变量,可是次全局变量的类型缺没有相应的更新过来,就会导致ts编译错误,此时就需要扩展全局变量的类型:

interface String{
    prependHello():string
}
'foo'.prependHello()

通过声明合并,使用interface+类型,可以给String添加属性或方法。
也可以使用declare namesace给已有的命名空间调加类型声明:

declare namespace JQuery{
    interface CusyomOptions{
        bar:string
    }
}

interface JQueryStatic{
    foo(Options:JQuery.CusyomOptions):string
}

jQuery.foo({bar:''})
在npm包或者UMD库中扩展全局变量

如之前所说,对于一个npm包或者UMD库的声明文件,只有export导出的类型声明才能被导入。所以对于npm包或者UMD库,如果导入词库之后会扩展全局变量,则需要使用另一语法在声明文件中扩展全局变量的类型,那么declare global

declare global
使用declare global可以在npm包或者UMD库的声明文件中扩展全局变量的类型:

declare global{
    interface String{
        prependHello():string
    }
}

export {}

'bar'.prependHello();

注意 即使此时声明文件不需要导出任何东西,仍需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局的声明文件。

模块插件

有时候import导入一个模块插件,可以改变另一个模块的结构。此时,如果原有模块以及有了声明文件,而插件模块没有声明文件,就会导致类型不完整,缺少插件部分的类型。ts提供了一个语法declare module,它可以用来扩展原有模块的类型。

declare module
如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用declare module扩展原有模块:

import * as mement form 'mement'
declare.module 'moment'{
    export function foo().mement.CalendarKey
}

也可以用于在一个文件中,一次性声明多个模块的类型

declare module 'foo'{
    export interface Foo {
        foo:string
    }
}

declare module 'bar' {
    export function bar(): string;
}
声明文件中的依赖

一个声明文件有时会依赖另一个文件中的类型,比如前面的declare module的例子中,我们就在声明文件中导入了mement,并使用了moment.CalendarKey这个类型:

import * as moment from 'moment';
declare module 'moment' {
    export function foo(): moment.CalendarKey;
}

除了可以在声明文件中通过import导入另一个声明文件中的类型之外,还有一个语法可以用来导入另一个声明文件,那即是三斜线指令

三斜线指令

namesapce类似,三斜线指令也是ts在早起版本中为了描述模块之间的依赖关系而穿凿的语法,随着es6的广泛应用,现在已经不建议在使用ts中的三斜线指令来什么模块之间的依赖关系了。
但是在声明文件中,他还是有一定的用武之地。
类似import,他可以用来导入另一个声明文件。区别在于,当以下几个场景中,我们CIA需要使用三斜线指令代替import:

  • 当我们在书写一个全局变量的时候
  • 当我需要依赖一个全局变量的声明文件时
书写一个全局变量的声明文件

这些场景听上去很拗口,但是实际上很好理解——————在全局变量的声明文件中,是不允许出现importexport关键字的。一旦出现了,那么他就会视为一个npm包或者UMD库,就不再是全局变量的声明文件了。所以在这种情况下,我们需要引用另一个库的类型,就必须用三斜杠指令了。

// types/jquery-plugin/index.d.ts
/// <reference types="jquery">
declare function foo(Options:JQuery.AjaxSettings):string

三斜杠指令的语法后面使用了xml的格式添加了对jquery类型的依赖,这样就可以在声明文件中使用JQuery.AjaxSettings类型了。
注意,三斜杠指令必须放在文件的最顶端,三斜杠指令的前面只允许出现单行或者多行的注释。

依赖一个全局变量的声明文件

在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过import导入,当然也就必须使用三斜线指令来引入

// types/node-plugin/index.d.ts
/// <reference types="node">
export function foo(p:NodeJS.process):string;

上面例子中,由于引入的是node中的全局变量的类型,他们是没有办法用过import来导入的,所以这种场景下也只能通过三斜线指令来引入。
以上两种情况下,只能通过三斜线指令引入。当前模块不支持import和要引入的声明文件不支持import

拆分声明文件

当我们的全局变量声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将他们一一引入,来提高代码的可维护性,比如:

// node_modules/@types/jquery/index.d.ts

/// <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;

其中用到了typespath两种不同的指令。他们的区别是:types用于声明对另一个库的依赖,而path用于声明对另一个文件的依赖。
上例子中,sizzlejquery是平行的另一个库,所以使用types来引入,其余的是将jquery拆分成几个不同的文件,通过path引入。

其他三斜线指令

除了这两种三斜线指令之外,还有其他三种指令。

/// <reference no-default-lib="true"/>, 
/// <amd-module /> 

但它们都是废弃的语法。

自动生成声明文件

如果库的源码就是由ts写的,那么使用tsc脚本将ts编译成js的时候,添加declaration选项就可以同时也生成.d.ts声明文件了。
我们也可以在命令行中添加-- declaration(简写-d),或在tsconfig.json中添加declaration选项。

  • declarationDir 设置生成 .d.ts 文件的目录
  • declarationMap 对每个 .d.ts 文件,都生成对应的 .d.ts.map(sourcemap)文件
  • emitDeclarationOnly 仅生成 .d.ts 文件,不生成 .js 文件
发布声明文件

当我们为一个库写好了声明文件之后,下一步就是将它发布出去,此时有两种方案:

  1. 将声明文件和源码放在一起。
  2. 将声明文件发布到@types下。
    这两种方案中有限选择第一种方案,保持声明文件和源码在一起,使用时候就不需要额外增加单独的声明文件库依赖了,而且也能保证声明文件的版本和源码的版本保持一致。
    仅当我们再给别人的仓库添加类型声明文件,但是原作者不愿意合并pull request时,CIA需要使用第二种方案,将声明文件发布到@types
将声明文件和源码放在一起

如果声明文件是通过tsc生成的,那么无须做任何其他配置,只需要吧编译好的文件也发布到npm上,使用方就能获取类型提示了。
如果是手动写的声明文件,那么需要满足一下条件之一,才能被正确的识别:

  • 给package.json 中的types或者typings字段指定一个类型的声明地址
  • 在项目根目录下,编写一个index.d.ts文件
  • 针对入口文件(package.json中的main字段指定的入口文件),编写一个同名不同后缀的.d.ts文件
    第一种方式:
{
    "name": "foo",
    "version": "1.0.0",
    "main": "lib/index.js",
    "types": "foo.d.ts",
}

指定了typesfoo.d.ts之后,导入此库的时候,就会去找foo.d.ts作为此库的类型声明文件了。
typingstypes一样,只是另一种写法。
如果没有指定typestypings,那么就会在根目录下寻找index.d.ts文件,将它视为此库的类型声明文件。
如果没有找到index.d.ts文件,那么就会寻找入口文件中的main文件指定的入口文件是否存在对应同名不用后缀的.d.ts文件。
类似这样:

    {
        "name": "foo",
        "version": "1.0.0",
        "main": "lib/index.js"
    }

package.json中不含有typestypings字符安,就会寻找是否存在index.d.ts文件。如果还不存在,那么就会寻找是否存在lib/index.d.ts文件。假如还是不存在,就会认为没有一个没有提供类型声明文件的库。
有的库为了支持导入子模块,比如import bar form 'foo/lib/bar',就需要额外在编写一个类型声明文件lib/bar.d.ts或者lib/bar/index.d.ts,这与自动生成声明文件类似,一个库中同时包含了多个类型声明文件。

将声明文件发布到@types

如果我们是给别人的仓库添加类型声明文件,但原作者不愿意合并pull request,那么就需要将声明文件发布到@types下。
与普通的npm模块不同,@types是统一由DefinitelyTyped管理。要将声明文件发布到@types下,就穿件一个pr,其中包含类型声明文件,测试代码,以及tsconfig.json等。
pull-request 需要符合它们的规范,并且通过测试,才能被合并,稍后就会被自动发布到 @types 下。

在 DefinitelyTyped 中创建一个新的类型声明,需要用到一些工具,DefinitelyTyped 的文档中已经有了详细的介绍,这里就不赘述了,以官方文档为准。

如果大家有此类需求,可以参考下笔者提交的 pull-request

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
vue-typescript-import-dts 是一个用于为 Vue.js 项目中的 TypeScript 文件生成类型声明文件的工具。在 Vue.js 项目中使用 TypeScript 进行开发时,我们经常需要为一些第三方库或自定义组件编写类型声明文件,以提供更好的代码提示和类型检查。 使用 vue-typescript-import-dts 工具可以自动分析 TypeScript 文件中的导入语句,并根据导入的模块生成对应的类型声明文件。这样,在使用该模块时,IDE 或编辑器就能提供准确的代码补全和类型检查。 例如,假设我们的项目中使用了一个名为 axios 的第三方库进行网络请求,但是该库并没有提供类型声明文件。我们可以通过 vue-typescript-import-dts 工具,在我们的 TypeScript 文件中导入 axios,并正确配置工具,它将自动为我们生成一个 axios.d.ts 类型声明文件。 具体使用 vue-typescript-import-dts 的步骤如下: 1. 在项目中安装 vue-typescript-import-dts,可以使用 npm 或 yarn 命令来安装。 2. 在 TypeScript 文件中,使用 import 语句导入需要生成类型声明文件的模块。 3. 在项目根目录下创建一个 .vue-typescript-import-dts.json 配置文件,用来配置生成类型声明文件的规则。可以指定生成的声明文件的输出路径、文件名等。 4. 运行 vue-typescript-import-dts 命令,它会自动扫描 TypeScript 文件中的导入语句,并根据配置生成相应的类型声明文件。 这样,在我们编写代码时,IDE 或编辑器就可以准确地为我们提供代码补全和类型检查的功能。这对于提高开发效率和代码质量非常有帮助。 总之,vue-typescript-import-dts 是一个便捷的工具,可以自动为 Vue.js 项目中使用的第三方库或自定义组件生成类型声明文件,提供更好的代码提示和类型检查功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值