类型文件: 掌握ts的关键

1,805 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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')

image.png

可以看到在 declaration-files.ts 中使用 axios这个变量就有了类型提示。

如果此时把 axios.d.ts 关闭(也就是需要把两个文件同时打开),就会报错,解决方法是把类型文件放在node_modules/@types里面,因为这个文件里面的类型定义文件ts编译器会自动读取。还有一种方法是在配置问津tsconfi.js中,在files配置项中把定义文件也进入即可。

image.png

如果 axios 不是一个函数,而是一个对象,对象里面有 get 和 post 方法呢?

interface IAxios {
  get: (url: string) => string
  post: (url: string, data: any) => any
}

declare const axios: IAxios

image.png

可以看到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属性定义的。

image.png

首先,利用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接口,对 ContextEggAppConfig 也增加了一些属性和方法。

declare module还有一个作用是对非ts/js文件模块进行类型扩充。ts 只支持模块的导入导出, 但是有些时候你可能需要引入 css/html 等文件,这时候就需要用通配符让ts把他们当做模块,下面是对".vue"文件的导入支持(来自vue官方):

image.png

声明把 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 所带来的便利。