一、前言
TypeScript 如今已经成为前端开发者必备的一项技能,它不经能够提升开发者的开发体验还能保障项目的可维护性,这主要得益于 TypeScript 的静态检测机制,可以帮助我们在编译的时候发现错误,当然如果你还是一个 any 大法的信仰者就当我没说。
当然今天的主题当然不是去介绍 TypeScript 的语法以及用法,今天重点介绍的主角是它如何去帮我们找到我们定义的类型
,这也是笔者最近遇到的问题,下面我先罗列几个问题,大家先思考下:
- node_modules 中 @types 文件夹有什么作用?TypeScript 是怎么找到他的?
- tsconfig 中的
typeRoots
和types
属性有什么作用?他们和类型查找有什么关系? - tsconfig 中的
files
、include
和exclude
属性有什么作用?他们和类型查找有什么关系? - TypeScript 声明文件有
全局库
和模块化库
两种?他们的特点是什么?怎么去自定义?
如果你都能顺利的答, 那么恭喜你 TypeScript 的类型定义以及查询规则这一块你已经顺利过关。
二、配置开发环境
其实总的来说在我们的项目中类型文件分为两种,一个是第三方库定义好的类型,一个就是自己项目中自定义的类型,1、2这两个问题属于第一类、3、4属于第二类,下面通过具体的实例分别解释,首先先配置好我们的开发环境,对于第三方库我们拿两个比较有代表性的库来分析,一个是 Jquery,一个是 ahooks 这是阿里开发的一个通用的hooks库,具体在这就不做过多介绍:
- 使用 create-react-app 创建 TypeScript 项目
npx create-react-app my-app --template typescript
#or
yarn create react-app my-app --template typescript
- 安装 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 到底是怎么找到这些类型的呢?这就得和typeRoots
、types
这两个属性关联起来了,首先要说的是其实第三方库的类型查找的规则和 node 的包查找是类似的,首先会在当前文件夹找 node_modules,在他下面递归的去查找,如果找不到就会去上层的目录找到 node_modules 目录,再递归查找,直到根目录。但是对于类型查找还是有些差别,下面拿 jquery 这个库来说:
- 在当前文件下找 node_modules 目录,发现没找到走道上一层
- 在根目录下找到 node_modules 目录,递归查找 jquery 包,发现存在
- 找到 jquery 包的 package.json 查找 types 属性发现没有,说明这个库是 js 编写的,去@types找他的类型定义
- 找到jquery的类型定义包,然后去找他的 package.json 文件并查找 types 属性,发现存在
- 找到 types 属性指定的文件(index.d.ts),这个文件就是 jquery 类型定义的入口(如果还不知道.d.ts和.ts文件的区别点击这里)
如果是使用 ts 编写的库如 ahooks 那么在第三步就可以结束,我们看下 ahooks 的package.json:
说明他的类型定义的入口就在这个包的 lib 目录下,至此大家对 ts 的类型查找应该有了一定的了解,但是可能就有同学会问了,你刚刚不是说的typeRoots
、types
这两个属性呢,他们的作用是啥?
typeRoots
和 types
都是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、总结
上面主要介绍了全局库
和模块化库
的一些使用,这里主要说一下files
、include
和exclude
属性,他们有什么作用。
我们首先要了解下什么样的文件才算是 ts 文件呢?答案如下:
- 拓展名为.ts、.tsx、.d.ts的文件
- 如果我们设置了
allowJs = true
,那么.js、jsx也会被视为 ts 文件
接着说files
、include
和exclude
这三个属性是控制着 ts 编译器的编译范围,他们各自的特点如下:
- files 是一个数组,数组的元素可以是相对路径和绝对路径
- inclue 和 exclude 属性是一个数组,但是组的元素类似
glob
的文件模式,比如*
、?
、**/
这种通配符的形式 - 如果同时设置了
files
和include
,那么编译器会把两者指定的文件引入,而exclude
只会对include
有效,对files
是无效的,即files
指定的文件如果同时被exclude
排除,那么该文件仍然会被编译器引入。
所以在引入 global.d.ts
时需要去配置 include
属性把它纳入编译范围之内,此时我们定义的变量才会被找到。