typescript 类型声明,抽象业务的第一能力...赶紧试试吧

463 阅读6分钟

wo.png

声明文件就是用typescript 的类型语言,描述库对外提供的变量,函数,模块的类型。javascript 是没有类型的,很多已经存在的库,都是用javascript 开发,在使用typescript 引用这类库的时候,那如何知道对应的类型,这就要借助声明文件。它主要有以下3个主要的关键点:

  • 声明文件格式:[filename].d.ts.

  • 它就实际上就是一个中间人的角色,为javascript 提供类型说明

  • 有的项目是直接使用typescript 开发,有时候也会使用声明文件去规划类型的设计, 把类型的声明和业务逻辑分离

    在设计类型时,只关注类型设计,完全是一种声明写法,使得typescript 拥有绝对抽象能力,不再关心实现的细节,对团队的协作,帮助巨大。

不管是为javascript 提供类型说明,还是作为类型设计,本质上一样的,只是使用目的上的区别。

声明参考

一些经常使用的数据格式(如:对象方法/属性、函数),我们看看他们的声明文件写法。

对象的属性方法

// 一个对象上对外提供方法/属性
export const obj = {
  getGreeting(str) {
    return `hello ${str}`;
  },
  count: 10,
};

/**
 * 声明写法
 * 使用declare namespace描述由点号访问类型或值。
 */ 
declare namespace myLib {
  function getGreeting(srt: string): string;
  let count: number;
}

函数重载

function getTypeList(input) {
  if (typeof input === "string") {
    return `hello world of ${input}`;
  } else if (typeof input === "number") {
    return [input];
  }
}


/**
 * 声明写法
 * 函数重载:函数名一样,参数不一样,返回的类型不一样
 */
declare function getTypeList(str: string): string;
declare function getTypeList(num: number): number[];

组织类型

当一个类库比较大的时候,就要考虑拆分根据职责,细分成不同的命名空间,不同的接口;这样降低复杂度,方便维护。

// 使用命名空间来组织类型,拆分为不同的接口

/**
 * 声明写法
 * 商品分类,我们列举其中2类,在同一个命名空间,用不的的接口去声明
 */

declare namespace Production {
  // 计算机
  interface Computer {
    // 内核数
    core: number;
    // 尺寸
    size: string;
  }
  // 水果
  interface Apple {
    // 单价
    price: number;
  }
}

// 也是支持嵌套命名空间, 通过圆点访问
declare namespace Production.Fruit {
  interface Banana {}
  interface Orange {}
}

类的声明

// 业务逻辑上的类
class Greeting {
  word = "";
  constructor(str: string) {
    this.word = str;
  }
  echo(str: string) {
    console.log(str || this.word);
  }
}

/**
 * 声明写法
 * 构造函数,方法,属性的声明
 */
declare class Greeting {
  constructor(str: string);
  word: string;
  echo(str: string): void;
}

全局变量

var globalVar = 'hello world'

/**
 * 声明写法
 */
declare var globalVar: string;

全局函数

// 在全局作用域上
var greet = (str) => console.log(str);=

/**
 * 声明写法
 */
declare function greet(greeting: string): void;

全局的直接全用declare 声明,局部的使用 declare namespace 命名空间,防止冲突。可以根据实际情况使用。

库结构分析

在为一个 JavaScript 库写声明文件时,首先要确定这个库的类型,是commonjs,es 模块,还是全局的类库,UMD Y库,他们都有一些主要特征,我们可以从下代码层面去分析:

  1. 没有明确引用,在任何地方直接使用require 或者 define
  2. 使用 import * as a from 'b'; or export c 声明引用、导出;
  3. 给 exports or module.exports 赋值
  4. 直接 window or global 对象赋值、

识别模块化的库

主要是一个函数,一个class类,commonjs,es 的模块, 文件档是都给出对应的模板。参数前面的声明参考,在理解的基础上去写,是比较简单的。

  • CommonJS/Node.js 样式的表单导入 var fs = require("fs");
  • 描述如何require或导入库的文档
  • 给 exports or module.exports 赋值
  • 使用 import * as a from 'b'; or export c 声明引用、导出;

识见全局库

全局库可以从调用上,声明的关键字上去找到一些线索:

  • 顶级var语句或function声明
  • 调用时 window.someName, 在任何地方直接调用
  • DOM 原语喜欢document或window存在的假设

识别全局库 UMD

在代码中,有一个立即执行函数:

(function (root, factory) {
    if (typeof define === "function" && define.amd) {
        define(["libName"], factory);
    } else if (typeof module === "object" && module.exports) {
        module.exports = factory(require("libName"));
    } else {
        root.returnExports = factory(root.libName);
    }
}(this, function (b) {

声明文件依赖

一个声明文件依赖其它的声明文件,声明文件会因类库不一样,引入的方式也会不一样

对全局库的依赖

如果您的库依赖于全局库,请使用/// 指令

/// <reference types="someLib" />
function getThing(): someLib.thing;

对模块的依赖

如果您的库依赖于某个标准es模块,请使用以下import语句:

import * as moment from "moment";

function getThing(): moment;

对 UMD 库的依赖

/// <reference types="moment" />
function getThing(): moment;

ES6 对模块调用签名的影响

//命名空间导入类似import * as moment from "moment"的作用一样const moment = require("moment")
// ES6 模块规范规定命名空间 import ( import * as x) 只能是一个对象, 不可以执行
import exp = require("express");
var app = exp();

在符合 ES6 的模块加载器中,顶级对象(这里导入为exp)只能有属性;顶级模块对象永远无法调用。

打开esModuleInterop将解决 TypeScript 转译的代码中的这两个问题。

  • 第一个改变了编译器的行为,
  • 第二个是通过两个新的辅助函数修复的,它们提供了一个 shim 来确保发出的 JavaScript 的兼容性:

模板的链接

抽象能力

在一个新需求澄清后,通常是要对业务进行抽象,一些需求细节还没有明确,一些依赖后台的接口字段还在讨论中,但是开发已经启动了,这个时候,typescipte 抽象能力,就排上用场了,可以忽略上面提到的细节。设计业务模块,抽象类,抽象参数

interface LoginResponse {
  userId: number;
  name: string;
}

interface LoginParams {
  name: string;
  pwd: string;
}

abstract class AbstractLogin {
  // 登录
  abstract login(params: LoginParams): Promise<LoginResponse>;
  // 登出
  abstract logout(): Promise<boolean>;
}

/**
 * 侧重的是抽象, 细节可以放在后面实现
 * 这是一个很重要的能力,可以先聚焦一点,其它的可以延后处理
 */
export class LoginUser implements AbstractLogin {
  login(params: LoginParams) {
    // do something, 不作细节处理了,实际会调用后台登录接口
    console.log(params);
    return Promise.resolve({ userId: 1, name: "hello world" });
  }
  logout() {
    // 实现的细节:如清空token,调用到后台接口注销,权限注销
    // 我们都可以搁置,可以放在后面提供,也可以由其它人来实现
    // 这样思路比较明确,降低代码维护成本
    return Promise.resolve(true);
  }
}

结语

在决定要为库写声明文件时,首先是要根据特征识别出是什么库,然后找到对应的模块参考一下。当然你如果对声明的方式,了然于胸,直接组织声明更好。实际上很多第三库都提供有声明文件,现在写声明文件更多的是用在类型设计上,为开发者提供业务抽象能力,赶紧试试吧。

文章中如有错漏,欢迎指正,谢谢。