搞懂TypeScript的类型声明

.d.ts 类型声明/定义文件是 TypeScript 重要的一部分, 希望本文可以带你搞懂下面几个问题:

  • 类型声明是怎么被TypeScirpt识别到的?
  • 怎么使用第三方包的类型声明文件?
  • TypeScript代码怎么编译生成类型声明文件?
  • 怎么为第三方包编写类型声明文件,第三方包类型错误怎么进行修正

类型声明文件

TypeScript 中有两种主要的文件类型 .ts.d.ts,以 .d.ts 为后缀的文件,我们称为 TypeScript 的类型声明文件,主要是用来描述模块内所有导出接口的类型信息(声明文件只是对类型进行定义用来做类型检查,不是代码实现,不能进行赋值)。

类型声明是怎么被引用到的

1.TypeScript 内置类型声明

当你安装 TypeScript 时,会自带很多类型声明:lib。其中 es5.full.d.ts 声明文件包含 JavaScript 运行时以及 DOM 中存在各种常见的环境声明。

img

以 DOM 类型为例:

const aEle = document.querySelector('a') // HTMLAnchorElement | null
const canvasEle = document.querySelector('#my_canvas') as HTMLCanvasElement

我们查找a标签,TypeScript 可以自动推断出类型为 HTMLAnchorElement | null

如果通过元素ID查找,TypeScript无法推断出具体的元素类型,我们可以使用类型断言来明确指定元素类型为 HTMLCanvasElement(关于断言内容可以阅读:搞懂TypeScript中的类型断言和使用场景)。当变量有明确的类型之后,我们也可以等到更好的类型校验和语法提示。

上面说到的 HTMLAnchorElementHTMLCanvasElement 就是 TypeScript 内置的 DOM 类型声明,也就是 <reference lib="dom" /> 的引用内容,具体内容可以参考:dom.generated.d.ts

2.第三方包自带的类型声明

一些第三方包会自带类型声明文件,通过 package.json 文件中的 "types""typings" 字段来指定声明文件位置,以 vue 为例:

{
  "name": "vue",
  "typings": "types/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "node": "./dist/vue.runtime.mjs",
        "default": "./dist/vue.runtime.esm.js"
      },
      "require": "./dist/vue.runtime.common.js",
      "types": "./types/index.d.ts"
    },
    "./dist/*": "./dist/*",
    "./types/*": "./types/*",
    "./package.json": "./package.json"
  },
  "scripts": {
    "build:types": "rimraf temp && tsc --declaration --emitDeclarationOnly --outDir temp && api-extractor run && api-extractor run -c packages/compiler-sfc/api-extractor.json",
    "test:types": "npm run build:types && tsc -p ./types/tsconfig.json",
    "format": "prettier --write --parser typescript "(src|test|packages|types)/**/*.ts"",
    "ts-check": "tsc -p tsconfig.json --noEmit",
    "ts-check:test": "tsc -p test/tsconfig.json --noEmit",
  }
}

vue 是使用 TypeScript 编写的,在 npm scripts build:types 中通过 tsc 命令来生成类型定义文件到临时文件夹 temp

tsc --declaration --emitDeclarationOnly --outDir temp

并通过 api-extractortemp 中的多个 .d.ts 进行处理和合并。

注意:package.json 使用 typestypings 来指定类型声明文件,而非 typetype 用来指定文件模块方案,通常为 modulecommonjs(默认)。

3.社区维护的声明文件

一些项目中使用的第三方包可能不是使用 TypeScript 编写的,没办法通过代码直接编译生成 .d.ts 声明文件,也没有在包内维护类型声明文件,我们就可以在 TypeScript 社区查找方案。

Definitely Typed是社区维护的声明类库,一些不是使用TypeScript编写的项目,我们可以找到想要的类型声明,极大的简化了JS项目迁移到TS的难度。安装的 @types/xxx 包,都是维护在 Definitely Typed 中。

比如@types/react就可以在 DefinitelyTyped 中找到。

默认情况下项目中安装的@types/react@types/react-dom 都会被编译器自动引入,TypeScript 会从当前目录向上查找所有 node_modules/@types 中的包,也就是包含 ./node_modules/@types../node_modules/@types 等目录。

但是如果在 tsconfig.json 中通过 typeRoots 配置了指定目录,就会自动引入该目录下的类型声明模块。

{
  "compilerOptions": {
    "typeRoots": ["./typings", "node_modules/@types"]
  }
}

注意:typeRoots 默认值为 “node_modules/@types” ,如果指定了其他目录就不会从默认目录中进行查找了,如果还想使用安装的 ”@types/xxx“ ,需要将默认值也添加进来。

怎么确认使用的第三方是否包含类型定义文件?

在 NPM 上会有对应的标签,使用TypeScript编写,会展示有TS标识:
image.png

DefinitelyTyped中维护类型定义,会展示有DT标识:

image.png

TypeScript编写的项目,怎么生成类型定义文件

使用TypeScript 开发的项目中可以通过 tsc 来编译生成类型声明文件,在tsconfig.json中配置一下编译选项:

// tsconfig.json
{
    "compilerOptions": {
        "declaration": true, // 用于指定是否在编译完成后生成相应的*.d.ts文件
        "emitDeclarationOnly": true // 只生成声明文件, 不会生成js文件
    }
}

也可以通过命令行指定参数:

tsc --declaration --emitDeclarationOnly

和上文介绍的 vue 一样,在 package.json 中通过 "types""typings" 字段来指定类型声明文件即可。

指定编译范围

指定待编译文件有两种方式:

  • 使用 files 属性
  • 使用 includeexclude 属性

如果 filesinclude 都未设置,那么除了 exclude 排除的文件,编译器会默认包含路径下的所有 TS 文件

如果同时设置 filesinclude ,那么编译器会把两者指定的文件都引入。

如果未设置 exclude ,那其默认值为 node_modulesbower_componentsjspm_packages 和编译选项 outDir 指定的路径。

exclude 只对 include 有效,对 files 无效。即 files 指定的文件如果同时被 exclude 排除,那么该文件仍然会被编译器引入。

前面提到,任何被 filesinclude 引入的文件的依赖会被自动引入。

为JS或者第三方代码编写.d.t

在使用TypeScript时,你可能遇到以下问题:

  • 自己使用JS编写的库,不想使用TypeScript重写,为其补充.d.ts
  • 项目中使用的第三方库没有类型声明文件
  • 第三方库缺少某些类型,或者错误定义了某些类型,需要对其进行补充和修正
1. 导入和导出类型定义

如果你想把某个模块相关的类型都定义在一个文件中,可以创建一个.ts 文件,并使用 export 导出,在其他文件中可以直接通过 import 引用。

2. 第三方包类型补充

某些库没有提供类型声明,或者声明类型和实际API不匹配,我们也可以在declare中对其进行补充。

declare可以用来声明全局变量、函数、类或者增强模块的类型:

  • 声明模块:当使用第三方库时,你可以使用 declare 关键字来声明模块的类型,这样可以避免 TypeScript 报错,也可以有更好的类型检查和自动补全。
// global.d.ts
declare module 'my-library-a';

上面的代码中我们声明了my-library-a,但是没有任何类型定义,就会的到隐私的any类型。使用第三方包时,TypeScript总提示类型错误或者缺少类型定义,又不想写类型,就可以使用这种方式。

如果我们只使用到了第三方库的某些方法,或者第三方库的一些类型出现错误,可以使用 declare 进行类型补充

// global.d.ts
declare module 'my-library-b' {  
    export function myFunction(): void;  
    export const myVariable: string;  
}
  • 声明全局变量、函数、对象
// global.d.ts
declare const A: string;  
declare function B(): void;  
declare interface User {  
    name: string;  
    age: number;  
}  
3. 生成.d.ts工具

微软还提供了一个为第三方库编写声明文件工具: dts-gen,使用dts-gen,可以 从任何 JavaScript 对象生成 TypeScript 定义文件 (.d.ts)。

如果你想为自己写的JS代码或者项目中引入的 NPM 模块编写类型声明,可以使用dts-gen,它会进行类型推断自动生成.d.ts文件。

最后,要让编写的类型声明文件(.d.ts)在项目中生效并使用,你需要将其添加到 tsconfig.json 文件的 includefiles 中。

{  
"compilerOptions": {  
// 其他配置项...  
},  
"include": [  
"src/**/*.ts",  
"typings/*.d.ts" // 假设你的类型声明文件在 typings 文件夹下  
]  
}  

在上述示例中,我们将 typings/*.d.ts 添加到 include 数组中,以确保 TypeScript 编译器会包含 typings 文件夹下的所有 .d.ts 文件。

tsconfig.json配置复用

我们可以使用 extends 来实现配置复用,即一个配置文件可以继承另一个文件的配置属性。

比如,建立一个基础的配置文件 configs/base.json

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

然后,tsconfig.json 就可以引用这个文件的配置了:

{
  "extends": "./configs/base",
  "files": [
    "main.ts",
    "supplemental.ts"
  ]
}

这种继承有两种特点:

  • 继承者中的同名配置会覆盖被继承者
  • 所有相对路径都被解析为其所在文件的路径
参考文档

理解 Typescript 配置文件

Definitely Typed

欢迎关注公众号“混沌前端”