• home > webfront > ECMAS > typescript >

    TS类型定义详解:types/typeRoots/@types,以及命名空间namespace

    Author:zhoulujun Date:

    什么是声明文件?声明文件就是给js代码补充类型标注 这样在ts编译环境下就不会提示js文件"缺少类型" 声明变量使用关键字declare来表示声明

    什么是声明文件?

    声明文件就是给js代码补充类型标注. 这样在ts编译环境下就不会提示js文件"缺少类型".

    声明变量使用关键字declare来表示声明其后面的全局变量的类型, 比如:

    // packages/global.d.ts
    declare var __DEV__: boolean
    declare var __TEST__: boolean

    看过vue3源码的同学一定知道这些是vue中的变量, 上面代码表示__DEV__等变量是全局, 并且标注了他们的类型. 这样无论在项目中的哪个ts文件中使用__DEV__, 变量ts编译器都会知道他是boolean类型.

    声明文件在哪里?

    首先声明文件的文件名是有规范要求的, 必须以.d.ts结尾, 

    为了规避一些奇怪的问题, 推荐放在根目录下.

    别人写好的声明文件( @types/xxx )

    当我们用 npm 等包管理工具安装第三方包的时候,有些包并不是 TypeScript 编写的,自然也不会导出 TypeScript 声明文件。这种情况下引入了这种包,则会编译报错(没有设置 allowJS——allowJS 是 TypeScript 1.8 引进的一个编译项)

    举个例子,当我们通过npm install jquery --save 安装 jquery 包并引用的时候,TypeScript 会报错。

    你可以通过npm install @types/jquery安装相关声明,或者自己定义一份.d.ts 文件,并将 jquery 声明为 module。’

    全世界不是 TypeScript 编写的包多了去了。

    在 TypeScript 大规模应用之前,社区已经有超过 90% 的顶级 JavaScript 库,或基于 Flow 编写的库(React系)。如果没有 DefinitelyTyped 项目,这些库想要提供类型支持,无疑只有完全重构代码。这既不现实也没必要。

    即使你的包是 TypeScript 编写的,如果你没有导出声明文件,也是没用的。(TypeScript 默认不会导出声明文件,只会编译输出 JavaScript 文件)。因此 TypeScript 必须对这种情况提供解决方案,而上面的两种方案:

    1. 安装 @types 

    2. 自己 declare module)就是 TypeScript 官方提出的,

    我的推荐是尽量使用 @types 下的声明,实在没有,再使用第二种方法。

    值得一提的是,并不是所有的包都可以通过这种方式解决的, 能解决的是 DefinitelyTyped 组织已经写好定义的包, 好消息是比较流行的包基本都有。 如果你想查一个包是否在 @type 下,可以访问 https://microsoft.github.io/TypeSearch/

    托管在github 上的 Definitely Typed(下文统一简称DT)项目是 Github 年度 octoverse 报告 上的常客,是贡献者数最多的前十个仓库之一——号称 GitHub review 数量之最的项目。

    具体查看《[翻译] DefinitelyTyped 的自动化管理改造: https://juejin.cn/post/6977281038263255054》

    TypeScript 经过了一系列的摸索,先后提出了 tsd(已废弃)、typings(已废弃),最终在 TypeScript 2.0 的时候重新整理了类型定义,提出了 DefinitelyTyped

    鉴于 DefinitelyTyped 的作用,我们说 DefinitelyTyped 让 TypeScript 再次伟大也不为过。

    DefinitelyTyped 就是让你把 "类型定义文件(*.d.ts)",发布到 npm 中,配合编辑器(或插件),就能够检测到 JS 库中的静态类型。

    类型定义文件的以 .d.ts 结尾,里面主要用来定义类型。

    类型定义

    我们可以使用 type 用来定义类型变量:

    // 基本类型
    type UserName = string
    
    // 类型赋值
    type WebSite = string
    type Tsaid = WebSite

    可以看到 type 其实可以定义各种格式的类型,也可以和其他类型进行组合。

    // 对象
    type User = {
      name: string;
      age: number;
      website: WebSite;
    }
    
    // 方法
    type say = (age: number) => string
    
    // 类
    class TaSaid {
      website: string;
      say: (age: number) => string;
    }

    当然,我们也可以使用 interface 定义我们的复杂类型,在 TS 中我们也可以直接定义 interface:

    interface Application {
        init(): void
        get(key: string): object
    }

    interface 和 type(或者说 class) 很像。

    • type 的含义是定义自定义类型当 TS 提供给你的基础类型都不满足的时候,可以使用 type 自由组合出你的新类型

    • interface 应该是对外输出的接口

    type 不可以被继承,但 interface 可以:

    interface BaseApplication {
        appId: number
    }
    
    export interface Application extends BaseApplication {
      init(): void
        get(key: string): object
    }


    declare

    declare的最广为人知的用处就是给第三方js库来做类型定义,让typescript明白js引入的用法,

    declare 可以创建 *.d.ts 文件中的变量,declare 只能作用域最外层:

    declare var foo: number;
    declare function greet(greeting: string): void;
    declare namespace tasaid {
      // 这里不能 declare
      interface blog {
        website: 'http://tasaid.com'
      } 
    }

    基本上顶层的定义都需要使用 declare, class 也是:

    declare class User {
      name: string
    }


    namespace

    为防止类型重复,使用 namespace 用于划分区域块,分离重复的类型,顶层的 namespace 需要 declare 输出到外部环境,子命名空间不需要 declare。

    // 命名空间
    declare namespace Models {
      type A = number
      // 子命名空间
      namespace Config {
        type A = object
        type B = string
      }
    }
    
    type C = Models.Config.A


    TypeScript 是怎么找定义的

    什么情况会找不到定义而报类似上面举的例子的错误

    包类型定义的查找

    就好像 node 的包查找是先在当前文件夹找 node_modules,在它下找递归找,如果找不到则往上层目录继续找,直到顶部一样, TypeScript 类型查找也是类似的方式。

    具体来说就是:

    • TypeScript 编译器先在当前编译上下文找 jquery 的定义。

    • 如果找不到,则会去 node_modules 中的@types (默认情况,目录可以修改,后面会提到)目录下去寻找对应包名的模块声明文件。

    @types/*模块声明文件由社区维护,通过发布到@types 空间下:https://github.com/DefinitelyTyped/DefinitelyTyped

    变量类型定义的查找

    和包查找类似,默认情况下变量类型定义的查找也会去 @types 下去寻找。只不过并不是直接去 @types 找,而是有一定的优先级, 这个过程类似原型链或者作用域链。

    const user: User = { name: "lucifer" };
    1. Typescript 则会先在本模块查找 User 的定义。

    2. 如果找不到, 则会到全局作用域找,而这个全局默认就是指的就是 @types 下的所有类型定义。(注意目录页是可以配的)

    也就是说 @types 下的定义都是全局的。当然你可以导入 @types 下导出的定义,使得它们的作用域变成你的模块内部。

    typeRoots 与 types

    前面说了 TypeScript 会默认引入node_modules下的所有@types声明,但是开发者也可以通过修改tsconfig.json的配置来修改默认的行为.

    tsconfig.json 中有两个配置和类型引入有关。

    1. typeRoots: 用来指定默认的类型声明文件查找路径,默认为node_modules/@types, 指定typeRoots后,TypeScript 编译器会从指定的路径去引入声明文件,而不是node_modules/@types, 比如以下配置会从typings路径下去搜索声明

      {  "compilerOptions": {    "typeRoots": ["./typings"]  }}

    2. types: TypeScript 编译器会默认引入typeRoot下所有的声明文件,但是有时候我们并**不希望全局引入所有定义**,而是仅引入部分模块。这种情景下可以通过types指定模块名只引入我们想要的模块,比如以下只会引入 jquery 的声明文件

      {  "compilerOptions": {    "types": ["jquery"]  }}

    总结就是:

    1. typeRoots 是 tsconfig 中 compilerOptions 的一个配置项,typeRoots 下面的包会被 ts 编译器自动包含进来,typeRoots 默认指向 node_modules/@types。

    2. types 和 typeRoots 一样也是 compilerOptions 的配置,指定 types 后,typeRoots 下只有被指定的包才会被引入。

    3. @types 是 npm 的 scope 命名空间,和@babel 类似,@types 下的所有包会默认被引入,你可以通过修改 compilerOptions 来修改默认策略。


    集成发布

    有两种主要方式用来发布类型定义文件到 npm:

    1. 与你的 npm 包捆绑在一起(内置类型定义文件)

    2. 发布到 npm 上的 @types organization

    前者,安装完了包之后会自动检测并识别类型定义文件。
    后者,则需要通过 npm i @types/xxxx 安装,这就是我们前面所说的 DefinitelyTyped ,用于扩展 JS 库的类型声明。

    内置类型定义文件

    内置类型定义就是把你的类型定义文件和 npm 包一起发布,一般来说,类型定义文件都放在包根目录的 types 目录里,例如 vue

    如果你的包有一个主 .js 文件,需要在 package.json 里指定主类型定义文件。

    设置 types 或 typeings 属性指向捆绑在一起的类型定义文件。 例如包目录如下:

    ├── lib
    │   ├── main.js
    │   └── main.d.ts # 类型定义文件
    └── package.json

    pageage.json

    {
        "name": "demo",
        "author": "demo project",
        "version": "1.0.0",
        "main": "./lib/main.js",
        // 定义主类型定义文件
        "types": "./lib/main.d.ts"
    }

    如果主类型定义文件名是 index.d.ts 并且位置在包的根目录里,就不需要使用 types 属性指定了。

    ├── lib
    │   └── main.js
    ├── index.d.ts # 类型定义文件
    └── package.json

    如果你发的包中,package.json 中使用了 files 字段的话(npm 会根据 files 配置的规则决定发布哪些文件),则需要手动把类型定义文件加入:

    {
      "files": [
        "index.js",
        "*.d.ts"
      ]
    }

    如果只发二级目录的话,把类型定义文件放到对应的二级目录下即可:

    import { default as App } from 'demo/app'


    发布到 @types organizatio

    发布到 @types organizatio 的包表示源包没有包含类型定义文件,第三方/或原作者定义好类型定义文件之后,发布到 @types 中。例如 @types/express

    根据 DefinitelyTyped 的规则,和编辑器(和插件) 自动检测静态类型。

    @types 下面的包是从 DefinitelyTyped 里自动发布的,通过 types-publisher 工具。

    如果想让你的包发布为 @types 包,需要提交一个 pull request 到 https://github.com/DefinitelyTyped/DefinitelyTyped

    在这里查看详细信息 http://definitelytyped.org/guides/contributing.html

    如果你正在使用 TypeScript,而使用了一些 JS 包并没有对应的类型定义文件,可以编写一份然后提交到 @types。

    发布到 @types organizatio 的包可以通过 https://microsoft.github.io/TypeSearch/ 搜索检索,使用 npm install --save-dev @types/xxxx 安装:


    具体推荐阅读《向微软官方贡献 @types 包后引发的思考: https://juejin.cn/post/6923379384002805774


    命名空间(namespace)是什么?

    什么时候要用命名空间?

    如果你发现自己写的功能(函数/类/接口等...)越来越多, 你想对他们进行分组管理就可以用命名空间, 下面先用"类"举例:

    namespace Tools {
        const TIMEOUT = 100;
    
        export class Ftp {
            constructor() {
                setTimeout(() => {
                    console.log('Ftp');
                }, TIMEOUT)
            }
        }
    
        export class Http {
            constructor() {
                console.log('Http');
            }
        }
    
        export function parseURL(){
            console.log('parseURL');
        }
    }

    仔细看你会发现namespace下还有export, export在这里用来表示哪些功能是可以外部访问的:

    Tools.TIMEOUT // 报错, Tools上没有这个属性
    Tools.parseURL() // 'parseURL'

    在js中命名空间其实就是一个全局对象. 如果你开发的程序想要暴露一个全局变量就可以用namespace;


    参考文章:

    types 和 @types 是什么? https://juejin.cn/post/6863654755248373774

    JavaScript 和 TypeScript 交叉口 —— 类型定义文件(*.d.ts) https://juejin.cn/post/6844903508991295501

    TypeScript系列


    转载本站文章《TS类型定义详解:types/typeRoots/@types,以及命名空间namespace》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/typescript/2021_1129_8715.html