持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情
本文是ts系列其中一篇,是总结自己在学习和工作中使用ts的心得感想,仅作为自己复习使用,如对您有启发不胜荣幸。
类型文件.d.ts是如何作用的
首先,在根目录下创建两个文件: axios.d.ts 和 declaration-files.ts。
axios.d.ts
类型文件没有任何代码实现,只是类型声明,比如interface, function, class 等,我们现在对axios这个变量进行类型定义,首先axios是一个函数,那么如何定义类型呢?
declare function axios(url: string): string
然后在 declaration-files.ts 加入如下代码:
axios('test.url')
可以看到在 declaration-files.ts 中使用 axios这个变量就有了类型提示。
如果此时把 axios.d.ts 关闭(也就是需要把两个文件同时打开),就会报错,解决方法是把类型文件放在node_modules/@types里面,因为这个文件里面的类型定义文件ts编译器会自动读取。还有一种方法是在配置问津tsconfi.js中,在files配置项中把定义文件也进入即可。
如果 axios 不是一个函数,而是一个对象,对象里面有 get 和 post 方法呢?
interface IAxios {
get: (url: string) => string
post: (url: string, data: any) => any
}
declare const axios: IAxios
可以看到axios有我们在类型文件中定义的 get 和 post 方法。
以上就是类型文件如何工作的。
写一个完整的类型文件
现在要给 calculator 写一个类型文件,calculator有如下功能
calculator('minus', [2, 3])
calculator('plus', [2, 3])
calculator.minus([2, 3])
calculator.plus([2, 3])
创建一个 calculator.d.ts 文件,首先 calculator 本身是一个函数:
type IOperator = 'plus' | 'minus'
type ICalculate = (operator: IOperator, numbers: number[]) => number
declare const calculator: ICalculate
其次,calculator 还有两个方法,那怎么办呢?使用 interface:
type IOperator = 'plus' | 'minus'
// type ICalculate = (operator: IOperator, numbers: number[]) => number
interface ICalculate {
(operator: IOperator, numbers: number[]): number
plus: (numbers: number[]) => number
minus: (numbers: number[]) => number
}
declare const calculator: ICalculate
创建 test.ts 文件,加入如下代码:
calculator('minus', [2, 3])
calculator('plus', [2, 3])
calculator.minus([2, 3])
calculator.plus([2, 3])
因为 calculator.d.ts 没有使用模块化导出,就相当于是一个全局变量,直接使用即可。如果想模块化导出呢?在 calculator.d.ts 加入 export default 进行模块化导出:
type IOperator = 'plus' | 'minus'
interface ICalculate {
(operator: IOperator, numbers: number[]): number
plus: (numbers: number[]) => number
minus: (numbers: number[]) => number
}
declare const calculator: ICalculate
export default calculator
test.ts
import calculator from './calculator'
calculator('minus', [2, 3])
calculator('plus', [2, 3])
calculator.minus([2, 3])
calculator.plus([2, 3])
这样就大功告成了,但是我们知道在 node_modules/@types 下的类型文件,ts 会自动读取,所以我们在node_modules/@types新建一个 calculator/index.d.ts,这样我们的引入方式就变成了import calculator from 'calculator'。
注意,这里类型文件的命名一定是index.d.ts
import calculator from 'calculator'
calculator('minus', [2, 3])
calculator('plus', [2, 3])
calculator.minus([2, 3])
calculator.plus([2, 3])
全局声明和局部声明
如果类型定义文件没有通过export导出,那么这个文件中包含的declare & interface & type就会变成全局声明。反之,若是通过export导出,那么这个文件包含的declare & interface & type则会是局部声明(模块声明文件),不会影响到全局声明。
npm包的类型定义文件相对于之前的全局声明文件而言,可以理解为是局部声明文件。只有当通过import引入npm包后,才能使用对应的声明类型。
其实也就是说,对于一个npm包的声明文件,只有通过export导出的类型,才能被使用。
declare module
npm下载的包自带了声明文件, 如果我们需要对其类型声明进行扩展就可以使用"declare module"语法。
我们可以看下vuex的官方库是如何在vue的实例上声明增加store属性定义的。
首先,利用declare module声明要扩充@vue/runtime-core包。然后,对ComponentCustomProperties接口进行扩充,因为它是vue3中实例的属性的类型。
还可以看下利用eggjs来开发项目时,扩充其类型定义文件:
import 'egg'
import { Model } from 'mongoose'
declare module 'egg' {
interface MongooseModels extends IModel {
[key: string]: Model<any>
}
interface Context {
genHash(plainText: string): Promise<string>
compare(plainText: string, hash: string): Promise<boolean>
}
interface EggAppConfig {
bcrypt: {
saltRounds: number
}
}
}
可以看到,在egg的类型文件中,扩展了 MongooseModels接口,对 Context 和 EggAppConfig 也增加了一些属性和方法。
declare module还有一个作用是对非ts/js文件模块进行类型扩充。ts 只支持模块的导入导出, 但是有些时候你可能需要引入 css/html 等文件,这时候就需要用通配符让ts把他们当做模块,下面是对".vue"文件的导入支持(来自vue官方):
声明把 vue 文件当做模块,同时标注模块的默认导出是component类型。
类型文件的导入导出方式
如果是以 esm 模块规范进行开发,那么声明文件以 export / export default 的方式导出
// 导出接口声明
export interface Options { }
// 声明默认函数
declare function main (options: Options): void
// 导出默认值
export default main
引入方式:
// 导入包
import * as demo from 'demo'
// 导入接口声明
import { Options } from 'demo'
如果 js 是 commonjs 风格的,则声明文件这样写:
// 包的声明
declare function main (options: main.Options): void
// 包里面的接口通过 namespace 声明
declare namespace main {
// 导出接口声明
export interface Options { }
}
// 导出包默认声明
export = main
Typescript有一个好处是,你可以将 ts 代码生成 CommonJs、AMD、ESM、UMS等规范,但是有些规范是无法兼容的,所以就有了 export =,将其统一,让ts支持以上规范。
【注意】
commonjs风格的导出只能默认导出一个,即export = main,如果你还想导出其他的接口,那么就需要使用namespace,并在里面进行导出其他接口类型。
引入方式:
// 导入包
import main from 'demo'
// 导入类型声明
import { Options } from 'demo'
所以,在TypeScript 中总共有如下几种导入导入方式,一般都是用import导入,如果用require导入是没有类型的。
// commonjs 模块
import * as xx from 'xx'
// es6 模块
import xx from 'xx'
// commonjs 模块,类型声明为 export = xx
import xx = require('xx')
// 没有类型声明,默认导入 any 类型
const xx = require('xx')
分析axios库类型定义文件
对类型文件进行精简,保留了重要部分。
- Record: 定义了请求头(AxiosRequestHeaders)的类型,即 key 只能是字符串,value 是一个联合类型 string | number | boolean
- &: 这个表示交叉类型, 响应头(AxiosResponseHeaders)有一个可选的key(set-cookie)
export type AxiosRequestHeaders = Record<string, string | number | boolean>;
export type AxiosResponseHeaders = Record<string, string> & {
"set-cookie"?: string[]
};
// 对请求的参数进行转换,此处对应的就是转换函数
export interface AxiosRequestTransformer {
(data: any, headers?: AxiosRequestHeaders): any;
}
// 定义了所有请求方法的类型
export type Method =
| 'get' | 'GET'
| 'delete' | 'DELETE'
| 'head' | 'HEAD'
...
// 定义了所有返回数据的类型,可以是二进制,也可以是json对象
export type ResponseType =
| 'arraybuffer'
| 'blob'
| 'document'
| 'json'
...
// 请求配置AxiosRequestConfig是一个接口,接口传入了一个泛型,这个泛型就可以流入到里面的属性或值里面。
export interface AxiosRequestConfig<D = any> {
url?: string;
method?: Method | string;
baseURL?: string;
headers?: AxiosRequestHeaders;
params?: any;
data?: D;
responseType?: ResponseType;
...
}
// 请求返回的数据类型,定义了一个interface,里面有data,status等,同时传入了一个泛型,这个泛型可以流入到data里面,这样我就定义了data的类型
export interface AxiosResponse<T = any, D = any> {
data: T;
status: number;
statusText: string;
headers: AxiosResponseHeaders;
config: AxiosRequestConfig<D>;
request?: any;
}
// 定义了一个Axios的类,这个类定义了构造函数constructor,拦截器interceptors,以及一些调用方法get/post等
export class Axios {
constructor(config?: AxiosRequestConfig);
defaults: AxiosDefaults;
interceptors: {
request: AxiosInterceptorManager<AxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
};
get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
post<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
...
}
// AxiosPromise 继承于 Promise<AxiosResponse<T>>,Promise<AxiosResponse<T>>里面传入了泛型,这样泛型流入到AxiosResponse里面
export interface AxiosPromise<T = any> extends Promise<AxiosResponse<T>> {
}
// 定义了一个Axios实例的接口
export interface AxiosInstance extends Axios {
(config: AxiosRequestConfig): AxiosPromise;
(url: string, config?: AxiosRequestConfig): AxiosPromise;
}
export interface AxiosStatic extends AxiosInstance {
create(config?: AxiosRequestConfig): AxiosInstance;
Axios: typeof Axios;
...
}
declare const axios: AxiosStatic;
export default axios;
首先我们引入import axios from 'axios',这个 axios 的类型就是 AxiosStatic,这个类型上有个create的方法:
export interface AxiosStatic extends AxiosInstance {
create(config?: AxiosRequestConfig): AxiosInstance;
Axios: typeof Axios;
...
}
执行create方法后,返回一个类型为 AxiosInstance 的变量:
export interface AxiosInstance extends Axios {
(config: AxiosRequestConfig): AxiosPromise;
(url: string, config?: AxiosRequestConfig): AxiosPromise;
}
这个变量首先是一个函数,这个函数可以这么使用(config: AxiosRequestConfig): AxiosPromise;,也可以这么使用(url: string, config?: AxiosRequestConfig): AxiosPromise;,同时它也继承了Axios类型,所以它有如下使用方法:
import axios from 'axios'
const axiosInstance = axios.create()
// 发送请求的三种方式
axiosInstance({ url: 'test.url'})
axiosInstance(url: 'test.url', config: { method: 'get' })
axiosInstance.get('test.url')
可以发现,我们通过解析axios的类型文件,就能基本把这个库的功能都搞清楚了,这就是 ts 所带来的便利。