一、前言

TypeScript 如今已经成为前端开发者必备的一项技能,它不经能够提升开发者的开发体验还能保障项目的可维护性,这主要得益于 TypeScript 的静态检测机制,可以帮助我们在编译的时候发现错误,当然如果你还是一个 any 大法的信仰者就当我没说。

当然今天的主题当然不是去介绍 TypeScript 的语法以及用法,今天重点介绍的主角是它如何去帮我们找到我们定义的类型,这也是笔者最近遇到的问题,下面我先罗列几个问题,大家先思考下:

  1. node_modules 中 @types 文件夹有什么作用?TypeScript 是怎么找到他的?
  2. tsconfig 中的 typeRootstypes 属性有什么作用?他们和类型查找有什么关系?
  3. tsconfig 中的filesincludeexclude 属性有什么作用?他们和类型查找有什么关系?
  4. TypeScript 声明文件有 全局库模块化库 两种?他们的特点是什么?怎么去自定义?

如果你都能顺利的答, 那么恭喜你 TypeScript 的类型定义以及查询规则这一块你已经顺利过关。

二、配置开发环境

其实总的来说在我们的项目中类型文件分为两种,一个是第三方库定义好的类型,一个就是自己项目中自定义的类型,1、2这两个问题属于第一类、3、4属于第二类,下面通过具体的实例分别解释,首先先配置好我们的开发环境,对于第三方库我们拿两个比较有代表性的库来分析,一个是 Jquery,一个是 ahooks 这是阿里开发的一个通用的hooks库,具体在这就不做过多介绍:

  1. 使用 create-react-app 创建 TypeScript 项目
npx create-react-app my-app --template typescript
#or
yarn create react-app my-app --template typescript
  1. 安装 jquery 和 ahooks
npm i -D jquery ahooks
#or
yarn add -D jquery ahooks

三、第三方库

1、Jquery

首先在 src 文件夹下新建一个 test.ts 文件,因为我们已经装了 jquery 我们可以直接使用全局变量 $ 了,来试试:


当我们输入 $ 后我们会发现编辑器既没有语法提示还出现报错,然后提示我们去安装 @types/jquery 这个包,这是我们和 @types 的首次见面,我们先去安装。


安装好之后我们再试试发现一切都是那么的美好,这里为什么出现这样的问题呢?其实就是 TypeScript 没有找到 $ 的定义,如果我们直接忽视它(禁止ts提示)然后直接运行发现项目还是可以直接运行起来的,因为我们装了这个包 webpack 当然就能找到这个包就没问题了呀。
话题回到主线上,此时我们去node_module目录下的@types目录下看一下:


果然他将 jquery 的定义文件安装到这个目录下,至于 TypeScript 是怎么找到的我们后面会说。

2、ahooks

下面还是在 index.ts 文件下去引入 ahooks 并使用了里面的 useUpdateEffect 这个 hook:

import {useUpdateEffect} from 'ahooks'
const Test = () => {
    useUpdateEffect(() => {
        console.log()
    },[])
}


这里我们发现编辑器并没有报错,同时也能正常展示出 useUpdateEffect 这个 hook 的类型定义,也就是说我们不需要引入对应的类型文件 TypeScript 就能找到他对应的类型定义。

3、总结

这里对于上面讲的两个库做一下总结,首先说一下 @types 这个目录的作用,它其实就是用来保存一些没有使用的 TypeScript 编写的库的类型定义文件,像 jquery 他就是用 js 编写的,而 ahooks 就是使用 ts 编写的。

那 TypeScript 到底是怎么找到这些类型的呢?这就得和typeRootstypes这两个属性关联起来了,首先要说的是其实第三方库的类型查找的规则和 node 的包查找是类似的,首先会在当前文件夹找 node_modules,在他下面递归的去查找,如果找不到就会去上层的目录找到 node_modules 目录,再递归查找,直到根目录。但是对于类型查找还是有些差别,下面拿 jquery 这个库来说:

  1. 在当前文件下找 node_modules 目录,发现没找到走道上一层
  2. 在根目录下找到 node_modules 目录,递归查找 jquery 包,发现存在
  3. 找到 jquery 包的 package.json 查找 types 属性发现没有,说明这个库是 js 编写的,去@types找他的类型定义
  4. 找到jquery的类型定义包,然后去找他的 package.json 文件并查找 types 属性,发现存在
  5. 找到 types 属性指定的文件(index.d.ts),这个文件就是 jquery 类型定义的入口(如果还不知道.d.ts和.ts文件的区别点击这里

如果是使用 ts 编写的库如 ahooks 那么在第三步就可以结束,我们看下 ahooks 的package.json:


说明他的类型定义的入口就在这个包的 lib 目录下,至此大家对 ts 的类型查找应该有了一定的了解,但是可能就有同学会问了,你刚刚不是说的typeRootstypes这两个属性呢,他们的作用是啥?

typeRootstypes 都是tsconfig 中 compilerOptions 的一个配置项,他们下面的包会被 ts 编译器自动包含进来,typeRoots 默认指向 node_modules/@types,这也就说明了为什么 ts 会去 @types 下面去找类型定义文件,既然这样说明我们也可以手动去调整 ts 的查找路径,比如我们本地用 ts 开发了一个通用的组件库,他的类型定义文件保存在 typings 目录下,那我们可以这么修改:

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

types有什么作用呢?如果不希望自动引入 typeRoots 指定路径下的所有声明模块,那可以使用 types 指定自动引入哪些模块。比如:

{
  "compilerOptions": {
    "types" : ["node", "lodash", "express"]
  }
}

那么就只会引入 node 、lodash、express 这三个声明模块,其它的声明模块则不会被自动引入。

四、自定义类型声明

自定义类型声明也分为两种,一个是全局库、一个是模块化库,它和 SCMAScript 2015 一样,任何包含顶级 import 或者 export 的文件都会被当成一个模块,相反,如果一个文件不带有顶级的import 或者 export 声明,那么它的内容被视为全局可见。

1、全局库

全局库就是指能在全局命名空间下访问,下面我们在根目录下创建一个 global.d.ts 文件:

declare const fuc:(a:number,b:number) => number
declare var n:number
declare type A = {
    name:string,
    age:number
}

在这里面我们通过 declare 声明了几个全局的变量,这里要注意了此时还是不能全局使用的,我们要去 tsconfig 文件下做一下配置:

"include": [
    "./**/*",
  ]

将我们的 include 属性改成这样,至于为什么这么改后面会讲到,然后我们就能在任何地方去使用我们上面定义的变量,比如我们在 index.js 文件下这样输入fuc:


发现编辑器给你提示了这个函数,并给出了他的类型定义。

2、模块化库

模块化类型库应该是我们开发过程中用的最多的,比如我们会在单独的一个.ts文件中定义一些公共的类型然后导出:

export const a: number = 1

export const add = (x: number, y:number) => x + y

export interface User {
  name: string,
  department: string
}

当然我们在导出模块也可以不去定义变量的类型,而在引入的模块中对引入的变量进行类型定义,如下所示:

//src/person.ts
export class Person {}

//src.index.ts
import { Person } from './person';
declare module './person' {
  interface Person {
    greet: () => void;
  }
}
Person.prototype.greet = () => {
  console.log('Hi!');
};

我们在 person.ts 中之定义了一个 Person 类,在 index.ts 中引入这个类通过 declare module 将 './person‘ 声明为一个模块,并对这个模块下面的 Person 增加对应的类型声明,这里就是给 Person 类的原型上增加了一个方法,这样我们就能正常的在原型上使用这个方法。

在第三方库中也可以去使用模块化库,比如 ahooks 中的 createUpdateEffect,我么看下他的.d.ts文件定义:

import type { useEffect, useLayoutEffect } from 'react';
declare type effectHookType = typeof useEffect | typeof useLayoutEffect;
export declare const createUpdateEffect: (hook: effectHookType) => effectHookType;
export {};

然后在 lib/index.d.ts 中通过import导入。

3、总结

上面主要介绍了全局库模块化库的一些使用,这里主要说一下filesincludeexclude属性,他们有什么作用。

我们首先要了解下什么样的文件才算是 ts 文件呢?答案如下:

  1. 拓展名为.ts、.tsx、.d.ts的文件
  2. 如果我们设置了 allowJs = true,那么.js、jsx也会被视为 ts 文件

接着说filesincludeexclude这三个属性是控制着 ts 编译器的编译范围,他们各自的特点如下:

  1. files 是一个数组,数组的元素可以是相对路径和绝对路径
  2. inclue 和 exclude 属性是一个数组,但是组的元素类似 glob 的文件模式,比如*?**/这种通配符的形式
  3. 如果同时设置了filesinclude,那么编译器会把两者指定的文件引入,而 exclude 只会对 include 有效,对 files是无效的,即files指定的文件如果同时被 exclude 排除,那么该文件仍然会被编译器引入。

所以在引入 global.d.ts 时需要去配置 include属性把它纳入编译范围之内,此时我们定义的变量才会被找到。

03-05 14:47