Typescript 书写声明文件(可能是最全的)

18,861

前言

对于为第三方模块/库写声明文件之前,我们需要知道第三方模块/库,是否需要声明文件,或者是否已有声明文件。

  • 若第三方模块/库,是ts编写且无声明文件, 可以使用--declaration配置选项来生成; 可以在命令行中添加 --declaration(简写 -d),或者在 tsconfig.json 中添加 declaration:true 选项
  • 若第三方模块/库,是js编写,分为两种情况:
1. 与该 npm 包绑定在一起,可以通过查找该库的`package.json`中的`types`属性
2. 发布到 @types 里,可以在官方提供的第三方声明文件库(http://microsoft.github.io/TypeSearch/)中查找

如若上面的情况都不符合, 则需要我们自己手写声明文件

前置知识

在书写声明文件之前,我们需要了解Typescript 相关知识, 可以自行查阅官方文档,或阅读我前一篇TypeScript 总结篇, 当然有需要写声明文件的需要,肯定是对Typescript有了解,可能已有相关实践,在此只是友情提示。 除此之外还需要对TS中的模块化有所了解,如下

模块化

模块(module)

主要是解决加载依赖关系的,侧重代码的复用。跟文件绑定在一起,一个文件就是一个module,模块写法和ES6一样。

命名空间(namespace)

同Java的包、.Net的命名空间一样,TypeScript的命名空间将代码包裹起来,通过export关键字对外暴露需要在外部访问的对象。 主要用于组织代码,解决命名冲突,会在全局生成一个对象,定义在namespace内部的都要通过这个对象的属性访问。

随着 ES6 的广泛应用,现已经不建议使用 ts 中的 namespace,而推荐使用 ES6 模块化方案,但在声明文件中,declare namespace 还是比较常用的。

namespace声明可以用来添加新类型,值和命名空间,只要不出现冲突。
与class/namespace等类型合并

/// 三斜线指令

///<reference types=“UMDModuleName/globalName” /> ts 早期模块化的标签, 用来导入依赖, ES6广泛使用后, 在编写TS文件中不推荐使用, 除了以下的场景使用///, 其他场景使用 import 代替
在声明文件中, 依赖全局库或被全局库依赖, 具体:

  1. 库依赖全局库, 因为全局库不能使用import导入
  2. 全局库依赖于某个 UMD 模块,因为全局库中不能出现import/export, 出现则为npm/UMD

注意: 三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释。

模块或一个 UMD 库依赖于一个 UMD 库,使用 import * as 语句引入模块

书写声明文件

第三方库使用场景:

  • 全局变量:通过 <script>标签引入第三方库,注入全局变量
  • npm 包:通过 import foo from 'foo' 导入,符合ES6 模块规范
  • UMD 库:既可以通过<script>标签引入,又可以通过 import 导入
  • 模块插件:通过 import 导入后,可以改变另一个模块的结构
  • 直接扩展全局变量:通过 <script> 标签引入后,改变一个全局变量的结构。比如为String.prototype 新增了一个方法
  • 通过导入扩展全局变量:通过import导入后,可以改变一个全局变量的结构

类库分为三类:全局类库、模块类库、UMD类库

全局变量

通过<script>标签引入第三方库, 注入全局变量 全局变量的声明文件主要有以下几种语法:

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

主要看下 declare namespace

// src/jQuery.d.ts

declare namespace jQuery {
    const version: number;
    class Event {
        blur(eventType: EventType): void
    }
    enum EventType {
        CustomClick
    }
    interface AjaxSettings {
        method?: 'GET' | 'POST'
        data?: any;
    }
    function ajax(url: string, settings?: AjaxSettings): void;
}

declare namespace声明全局命名空间,去掉declare namespace, 即从中提出代码,再在前面加上declare即是声明各全局变量

// 声明全局函数,其他同理
declare function ajax(url: string, settings?: any): void;

npm 包

在npm包中, 通过import foo from 'foo'导入npm包。npm 包的声明文件主要有以下几种语法:

export // 导出变量
export namespace // 导出(含有子属性的)对象
export default // ES6 默认导出
export = // commonjs 导出模块

export

在 npm 包的声明文件中,使用 declare 不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 export 导出,然后在使用方 import 导入后,才会应用到这些类型声明。

与非声明文件写法类似, 使用import导入, ES6模块语法

混用 declare 和 export

使用 declare 先声明多个变量,最后再用 export 一次性导出
注: interface 前是不需要 declare 的

export default

注意,只有 function、class 和 interface 可以直接默认导出,其他的变量需要先定义出来,再默认导出

export namespace

用来导出一个拥有子属性的对象

export =

在 commonjs 规范中, 使用exports/module.exports导出模块, 针对这类模块的声明文件,需要使用export =导出

declare module "a" {
    export let a: number
    export function b(): number
    export namespace c{
        let cd: string
    }
}


import * as A from 'a'
A.a //
A.b()
A.c.cd // 
// 函数
declare module 'app' {
  function fn(some:number):number 
  export = fn
}

const app = reqiure('app')
app()// 调用fn
// 变量/常量
declare module 'let' {
  let oo:number = 2
  export = oo
}

const o = reqiure('let')
o // 使用

UMD 库

既可以通过 <script> 标签引入,又可以通过 import 导入的库,称为 UMD 库。相比于 npm 包的类型声明文件,需要额外声明一个全局变量,为了实现这种方式,ts 提供了一个新语法 export as namespace

export as namespace
作用: 局部变量生命为全局变量
一般使用 export as namespace 时,都是已有 npm 包的声明文件,再基于它添加一条 export as namespace 语句,即可将声明好的一个变量声明为全局变量

// types/foo/index.d.ts
export as namespace foo; // 全局导出
export = foo; // or export default foo; 导出npm模块
declare function foo(): string;
declare namespace foo {
    const bar: number;
}

直接扩展全局变量

// global.extend.d.ts
interface String {
    prependHello(): string;
}
// src/index.ts
'xx'.prependHello()

在 npm/UMD 中扩展全局变量

对于一个 npm 包或者 UMD 库的声明文件,只有 export 导出的类型声明才能被导入 导入此库之后可以扩展全局变量, 需要使用 declare global

// types/foo/index.d.ts

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

export default function foo(): string;
// src/index.ts

import foo from './foo'
'bar'.prependHello()

模块插件

导入一个模块插件, 改变原有模块结构, 原有模块已有声明文件, 导入的模块插件没有声明文件

// types/moment-plugin/index.d.ts

import * as moment from 'moment'; // 原有模块

declare module 'moment' {
    export function foo(): moment.CalendarKey;
}
// 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();

其他

shims-vue.d.ts

Ambient Declarations(通称:外部模块定义) ,主要为项目内所有的 vue 文件做模块声明,毕竟 ts 默认只识别 .d.ts、.ts、.tsx 后缀的文件;(即使补充了 Vue 得模块声明,IDE 还是没法识别 .vue 结尾的文件,这就是为什么引入 vue 文件时必须添加后缀的原因,不添加编译也不会报错)

shims-jsx.d.ts

JSX 语法的全局命名空间,这是因为基于值的元素会简单的在它所在的作用域里按标识符查找(此处使用的是**无状态函数组件 (SFC)**的方法来定义),当在 tsconfig 内开启了 jsx 语法支持后,其会自动识别对应的 .tsx 结尾的文件,可参考官网 jsx

语言类型分类

静态类型、动态类型和弱类型、强类型
静态类型:编译期就知道每一个变量的类型。类型错误编译失败是语法问题。如Java、C++
动态类型:编译期不知道类型,运行时才知道。类型错误抛出异常发生在运行时。如JS、Python
弱类型:容忍隐式类型转换。如JS,1+'1'='11',数字型转成了字符型
强类型:不容忍隐式类型转换。如Python,1+'1'会抛出TypeError

小结

关于我对声明文件的实践,详见ts-declare, 另外,关于文章中,不对/不妥之处,欢迎指出,感谢~

参考

www.tslang.cn/docs/handbo… ts.xcatliu.com/basics/decl…
zhuanlan.zhihu.com/p/58123993
blog.poetries.top/2019/09/03/…
juejin.cn/post/684490…