Typescript 声明合并的一些使用

vueTypescript支持文章中,提到一个增强类型以配合插件使用
我们的代码中也经常出现这些,原理就是基于Typescript申明合并(Declaration Merging)特性

Declaration Merging 声明合并

先贴一波描述

TypeScript中有些独特的概念可以在类型层面上描述JavaScript对象的模型。
这其中尤其独特的一个例子是“声明合并”的概念。 理解了这个概念,将有助于操作
现有的JavaScript代码。 同时,也会有助于理解更多高级抽象的概念。
对本文件来讲,“声明合并”是指编译器将针对同一个名字的两个独立声明合并为
单一声明。 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被
合并;不局限于两个声明。

下图声明了哪些类型相互间不可以声明合并

Declaration Type Namespace Type Value
Namespace X X
Class X X
Enum X X
Interface X
Type Alias X
Function X
Variable X

我们来罗列一些常用的场景

常见的场景

日常开发中,常见的使用申明合并(Declaration Merging)场景有

当使用的依赖不是ts编写的或者没有typings申明文件package.json中没有typings

  • 如果@types/package-name存在,可以安装@types/package-name来添加这个依赖的ts声明

    1
    npm install @types/package-name --save-dev
  • 如果没有@types/package-name,可以自己手写声明文件,为这个库添加ts声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // js-cookie.d.ts
    declare module "js-cookie" {
    interface cookie{
    get(key:string):string?
    // ....
    }
    const Cookie:cookie
    export default Cookie
    }

    为我们现有的依赖添加自定义属性或方法的声明

  • 开发过程中我们有时候会为一些依赖添加一些属性或者方法,为了能全局感知,我们可以为当前的依赖添加一个属性额外声明
    比如vue官网的这个例子:
    vue声明一个 string 类型的实例 property $myProperty 来源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 1. 确保在声明补充的类型之前导入 'vue'
    import Vue from 'vue'

    // 2. 定制一个文件,设置你想要补充的类型
    // 在 types/vue.d.ts 里 Vue 有构造函数类型
    declare module 'vue/types/vue' {
    // 3. 声明为 Vue 补充的东西
    interface Vue {
    $myProperty: string
    }
    }
  • vuex的类型系统虽然有些小毛病,但是一些小小的操作也能为我们的代码开发带来幸福感
    我们可以尝试为 vuexmapState 添加类型支持

    假设我们已经为vuewStore中的rootState做了如下类型声明:

    1
    2
    3
    4
    interface RootState{
    userName:string
    userId:number
    }

    我们可以为mapState这个方法添加如下声明

    1
    2
    3
    4
    5
    declare module 'vuex/types/helpers' {
    interface Mapper<R> {
    <Key extends keyof RootState>(map: Key[]): { [K in Key]: () => RootState[K] }
    }
    }

    这时候在你的vue代码中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import { mapState } from 'vuex'
    const states = mapState(['userName','userId'])
    // 会看到类型有了提示
    // states的类型为 => { userName: () => string; userId: () => number; }
    export default {
    computed:{
    ...mapState(['userName','userId'])
    },
    methods:{
    test(){
    this.userName // => string
    this.userId // => number
    }
    }
    }

    虽然我们不能改变vue$store的类型,但是通过这个方法,我们可以把mapState的结果变为我们预期的呀
    mapActionsmapMutations等我们也可以像这样去修改

  • 声明一些全局的方法变量
    代码中经常会有一些自定义的全局变量或者方法,用着用着就忘了是什么类型了,用着用着就忘了该传什么参数了。
    为这些对象添加申明,这样以后每次只要记得名字就好了 申明一次,提示永久
    比如:声明window.imagePreview

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { ImagePreviewOptions } from 'vant'
    declare global {
    interface Window {

    /**
    * 预览图片
    */
    imagePreview(option: ImagePreviewOptions, startPosition?: number): void
    }
    }
  • 声明环境变量变
    process.env经常用到,但又不是天天用,每个项目还定义不一样
    这时候为process.env添加一个申明就变得有意义了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    declare global {
    namespace NodeJS {
    interface ProcessEnv extends Dict<string> {
    /**
    * 当前环境
    * - `local` 本地开发环境
    * - `production` 正式环境
    * - `dev` 测试环境
    * - ....
    */
    CODE_ENV: 'local' | 'production' | 'dev'
    /**
    * 可以判断当前`vue-cli-service` 的参数
    * - `development` -> `vue-cli-service serve`
    * - `production` -> `vue-cli-service build`
    */
    NODE_ENV: 'development' | 'production'
    }
    }
    }
  • 改变一些代码组织结构

    一般我们定义接口声明的时候,会一起定义依赖的参数和返回,大概就是下面这个样子
    有一个小小的尴尬,随着接口的增加,这个文件下面export出来的东西就越来越多

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // #region 查询用户列表
    /**
    * 获取用户信息返回
    */
    export interface GetUserListResponse {
    /**
    * 接口状态码
    */
    code: number
    /**
    * 接口返回信息
    */
    message?: string
    /**
    * 返回的数据
    */
    data: any
    }
    export interface GetUserListQuery {
    userName?: string
    }
    /**
    * 查询用户列表
    */
    export function getUserList(queryParam: GetUserListQuery): Promise<GetUserListResponse> {
    return {} as any
    }
    // #endregion

    借助functionnamespace可以申明合并,也可以写成下面这个样子
    这样我们每个接口就只会暴露出一个命名空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /**
    * 查询用户列表
    */
    export function getUserList(queryParam: getUserList.Query): Promise<getUserList.Response> {
    // @todo
    return {} as any
    }
    export namespace getUserList {
    /**
    * 查询用户列表
    */
    export interface Response {
    /**
    * 接口状态码
    */
    code: number
    /**
    * 接口返回信息
    */
    message?: string
    /**
    * 返回的数据
    */
    data: any
    }
    export interface Query {
    userName?: string
    }
    }

    当然,声明合并的用途还很多,我也只是一个typescript小学生,若有不正之处,还请多多指教