阅读 7701

TS in JS 实践指北

头图

不知道有多少 TS 爱好者哀叹过这个问题:虽然我很想用 TS,奈何老大只让用 JS。今天我,告诉你,在 JavaScript 中也可以很流畅的使用 TypeScript ,而且你的老大不会找你的麻烦。

系列文章

第一期:《ts安利指南》

本期:《TS in JS 实践指北》

写在前面

很多同学在看了《ts安利指南》后,评论说道:"TS 虽然香,奈何我们老大没兴趣。" "有兴趣,但是需要时间慢慢推行,没办法啊。" 当时我就觉得,那我安利的不到位啊,于是做了很长时间的准备,写出了这篇续集,讲述了我在 JS 里面苦苦探索 TS 实践的结果。

如果你有 TS 基础,读起来会很顺畅。如果没有 TS 基础,也能看懂一些(这也是我的目标),但是如果有会理解的更深一些。如果你不喜欢 TS,emmmmmm...就当看个乐吧 :P

本文是针对最新版的 VSCode(v1.41.0) 下所写的。其他 IDE 如果有出入,以 IDE 官方能力为准。

WebStorm 也支持,但是因为它包含了自家特色的智能引擎,隔壁行八竿子打不到的代码也能在当前作用域找到,所以这种神器在这里不做讨论。

什么,你用记事本?

-w136

友情提示:主依赖环境只有最新版的 VSCode,不需要额外的配置。本文一些主要的 demo,有需要的同学可以在这里找到

作用原理

VSCode 里的 TS

TypeScript 是 2012 年由 Microsoft 推出的一门工具。然而因为生不逢时,在 VSCode 出来之前,它在国内并没有受到广泛的关注。

有一部分人意识到它有能改变 JS 弱点的潜力,开始拥抱并应用这门技术,这批人很多是有后台开发的背景。他们更喜欢静态类型语言所带来的好处,并且最为关键的是,他们使用 Visual Studio、Eclipse 这些支持 TypeScript 的 IDE。但是纯前端同学很少会去选择一个这么重的 IDE。在那个时候,纯前端大部分用的是类似 Sublime 这种轻量级的 IDE,有一点残疾的智能效果(俗称智障),而他们都不支持 TypeScript ,写起来就像是用记事本写代码的感觉。用记事本干活,可能会活到自闭。因此当那帮“全栈大佬”拍屁股走人之后,接手的同学苦不堪言,把 TS 恨到了骨子里了。

在 VSCode 出来之后,前端开发终于有了一个专业的轻量级 IDE。随着 VSCode 在前端开发里的普及,TS 被更多 前端开发er 所接受,开始迎来了它的时代。在使用 VSCode 写 JS 代码的时候,不管你喜不喜欢、用不用 TS ,都会在不经意的时候,享受它所带来的便利。

JS 里的 TS

不知道大家有没有想过,为什么在 JS 中打出document.的时候,VSCode 就会自动弹出它里面的方法。

document funciton

明明属于 TS 的能力范围,也影响到了 JS ?

其实这也是 TS 成立之初所期望达成的目标:Advanced type system & Developer tooling support。翻译过来就是:高级的 type 系统和对开发工具的支持。TS 本身就是 JS 的超集,因此对 JS 有一定支持也是它的 kpi 之一。

VSCode 在 JS 环境下的 TS 能力来自于 VSCode 自己揣着的 TS 库。VSCode IDE 内置了 node_modules 文件夹,里面就有 TypeScript 的包。而在 TypeScript 的文件夹下有一些非常基础的 api 的 .d.ts 声明文件

lib.dom.d.ts

看这张图是不是有很多熟悉的方法的名字?它提供了 Dom 相关方法的能力。因此在 JS 里面本身,靠着这份文件,就可以有提示 Dom Api 的能力。

在 JS 里面,TS 使用的范围其实比你想象中的多很多。

优雅的头文件

我们来谈谈这个在 JS 里带来提示能力 .d.ts 声明文件。

内置声明文件

就像上面提到的,一些基础 Api 在 VSCode IDE 直接内置了。TS 成立之初的另一个目标是:Support for modern JavaScript features。大白话就是:为 JS 的现代能力提供支持。

因此 VSCode 内置了 Dom 和 ES2015、ES2016、ES2017 ... 的语法特性也不奇怪了。但是 VSCode 默认只开启了到 ES2016 能力的支持,需要更多语法可以在根目录下新建一个名为jsconfig.json的文件,然后输入以下内容:

{
    "compilerOptions": {
        "lib": [
            "dom",
            "esnext"
        ]
    }
}
复制代码

你甚至可以得到 ES2020 的语法提示。

matchAll

由于 VSCode 自带的声明文件只支持由 ECMAScript 和 W3C 所制定的特性,但是我们开发中需要的 Api 远远不止这么一点,因此就有非常多的第三方的声明文件出现。

包内自带的声明文件

不指定默认入口

有的 JS 文件会自带声明文件。只要声明文件的前缀和 JS 文件前缀相同,VSCode 就会自动引入声明文件。

比如:

// urlLib.js
export function url1 (str) {
    return 'url1'
}
export function url2 (str) {
    return 'url1'
}
export function url3 (str) {
    return 'url1'
}
复制代码
// urlLib.d.ts
export function url1(str: string): string
export function url2(str: string): string
export function url3(str: string): string
复制代码

.d.ts

这里有一点需要注意的是,IDE 会以声明文件定义的 Api type 优先。如果声明文件里面没有包含对应 .js 文件的某个暴露的方法,IDE 也不会给出存在这个方法的提示,甚至在开启语法检查的时候还会报错。

另外 VSCode 引入声明文件有自己的一套顺序规则,类似 node 的 require 规则。太长了就不放出来了,有兴趣的同学可以点这里了解详情。

指定默认入口

包内自带的声明文件还可以不和源码放一起,单独放在某个文件夹维护,只要在 package.json 中指定声明文件的入口,VSCode 就会自动去找这个文件。比如 Vue 的 package.json 里面

vue package

typings或者types的属性下指定入口,当我们直接import Vue from 'vue'的时候,就会去寻找指定的声明文件。

import vue

来自 @types 的声明文件

还有一些包他们并不把声明文件放在自己的目录下面,而是托管在一个专门放声明文件的私有域下。私有域里最有名的包可能就是 @types/node 了。

如果你想使用 node 的声明文件,你需要安装 @types/node。

npm i @types/node -D

这样使用 node.js 的 Api 就有了智能提示。

node

有时候你可能会发现没有安装@types/node,IDE 也会提示 node.js 的 api。那是因为 VSCode 默认还会在几个全局的地方找 types 包,而你正好有,那就顺理成章有了提示。

你可能会好奇这个@types是个什么域。答案是:Microsoft 的域。符合规范的的声明文件可以发布到这个地方,供全世界的程序猿/媛使用。如果你想拥有某个 node_modules 的提示,但是包里面并没有提供声明文件,可以去这里找找

search jquery

当然你也可以按照规范写一个声明文件发布上去供其他人使用,点击这里去按要求提个 pr 就可以了。

应用:使用 .d.ts 声明文件拓展 type 能力

用声明文件增加 type 能力是无感知的,使用者并不需要关注声明文件的内容,非常优雅。就算坐你旁边的程序员很讨厌 TS,这种方式也可以确保他在使用过程中几乎不会接触到 TS 的代码。

团队里的公共组件

前端团队内部会有很多自己的公共方法,我们可以为内部维护的一些公共方法添加声明文件,让其他人使用的时候能够享受智能提示的福利,降低代码出错的可能性。

比如下面这份针对 url lib 的声明文件:

// url.module.d.ts

/**
 * url lib 工具包
 */

/** 格式化url */
export function formatUrl (a: string): string

/** 获取url参数 */
export function getParams (paramName: string): string
复制代码

保持 .d.ts 声明文件和 lib.js 在一个目录下并且同名,比如 /xxx/url.module.js 和 /xxx/url.module.d.ts

就可以这样使用

import * as urlLib from "./url.module"
复制代码

demo

IDE 会帮你自动引入url.module.d.ts声明文件。

全局变量

在前端业务里面,往window下塞全局变量的操作很常见。填充的内容可以是一些工具,检测环境的函数等。在代码里面通常用这种语法 window.XXCompanyLibs.url.format来调用函数。但是window.XXCompanyLibs下有哪些函数通常需要另外去看文档或者源码。当团队来了新同学,需要用到这些公共函数的时候会一脸懵逼,然后在邮件里吐槽这块做的真不方便。

我们可以使用声明文件,往全局作用域声明一个对象,这样在这个库里写代码的其他小伙伴就能发现全局作用域下有了这个全局变量,并感受到来自于你的善意。

// gbobal.d.ts
interface ILib {
    /** 获取当前环境 */
    getEnv: string
    /** img方法 */
    img: {
        formatImg: (a: string, b: string) => string
    }
    /** url方法 */
    url: {
        formatUrl: (a: string) => string
    }
}

/**
 * xx公司全局变量
 */
declare var XXCompanyLib: ILib // 定义全局变量
复制代码

demo

这里涉及到了声明类型暴露方式的知识点,有兴趣更深入了解的同学可以点这里去深造。

神奇的注释

说到在 JS 当中的注释,大家马上会想到 ///* */

然而在这里我将要介绍的是这种注释 /** */ ,也就是 JSDoc 规范的注释。

/** 我是 JSDoc 注释 */
function foo () {}
复制代码

JSDoc

JSDoc 是一种注释规范,也是一种生成文档的工具,大家或多或少都接触过。

这种规范由于格式美观,受到很多程序员小哥哥小姐姐的欢迎。也有很多牛x的前端库在使用它作为注释规范,比如 lodash。

在这些之外,JSDoc 还有一些大家可能不了解的功能,我们就从这里开始。

JSDoc 解决了什么问题

如果代码的作用域清晰,VSCode 本身可以通过上下文去识别关系,做到自动关联有逻辑关系的数据。比如定义一个变量,下一行使用它的时候,VSCode 会知道这个变量是从哪来的。

但是在自定义的函数里面,IDE 不知道传参的类型,因此这些函数里的入参缺失了 type,成了 any。如果返回的类型也是 any,那函数赋予的变量也缺失了 type。随着这种函数越来越多的时候,一半的代码就成了 any script。

function foo (a, b) {
    document.querySelector('xxx') // 这里 IDE 知道 document.querySelector 是什么,以及返回了什么
    console.log(a) // 这里 IDE 不知道 a 是什么,当 any 处理
    console.log(b) // 这里 IDE 也不知道 b 是什么,也当 any 处理
    return a // return any
}

var bar = foo(1, 2) // 由于在 foo 里面不知道 a 是什么,所以 bar 的类型成了 any
复制代码

另外一方面,如果我们人为修改了this,也会引起 any script。因为this在运行时才能确认,在面对不清晰的this作用域链时, VSCode 不能理解你™对this做了些什么手脚。这种问题经常出现在公共方法的返回值当中。

上面提到的这些,都可以通过 JSDoc 去解决。

你知道的 JSDoc

国际惯例,先从简单的开始铺垫。

我们在代码中写一个function的时候,为了下一个接坑的同学不会来找你的麻烦,通常会去注释一下。

使用 JSDoc 风格的注释长下面这个样子:

/**
 * 方法:foo
 * @param {string} name
 */
function foo (name) {
    console.log(name)
}
复制代码

这里的/** ... */就是一个典型的 JSDoc 的语法。其中@param表示该方法接收一个名为namestring类型的参数。

在支持 JSDoc 的 IDE 当中,比如 VSCode 和 WebStorm,在使用这个方法 hover 的时候还会给出比较友好的提示。

demo

你可能不知道的 JSDoc

在上面的基础上,我们进一步来研究下面这个代码。

/**
 * 方法:foo
 * @param {string} name
 */
function foo (name) {
    console.log(name)
    return name
}

/**
 * 方法:bar
 */
function bar (name) {
    console.log(name)
    return name
}

var _foo = foo('name') // _foo 为 string
var _bar = bar('name') // _bar 为 any
复制代码

这里有一个bar的方法,少了@param,并且把入参都返回了出来。我们来看下他们返回值是什么类型:

type demo

可以看到,拥有@paramfoo函数返回出来的参数也是 string 类型,而bar函数返回的是 any。

JSDoc 为函数补充了入参类型,使得 IDE 能够识别出来函数入参出参的关系。

另外一方面,在函数内部,有了入参类型的foo函数的参数name,IDE 给了它提示string内方法的能力。

string type

这就是 JSDoc 解决 JS 里函数缺失 type 能力的方法。 JSDoc 里@param的这个标记,在{}中间代表的就是一个 TS 的 type 类型。

有关@param文档点这里

类似的,在上面的基础下,我们再改一下 JSDoc 的内容。

/**
 * 方法:foo
 * @param {{firstname: string, nameLength: number}} name
 */
function foo (name) {
    console.log(name)
    return name
}
复制代码

这时函数内的 name 就不是一个string了,而是一个自定义的object类型。当然,当我们访问name时,会得到firstnamenameLength两个属性。

demo

还记得在《ts安利指南》中提到过的"配置文件自动提示"吗?

/**
 * webpack配置自动提示
 *
 * 先安装对应的types包: `npm i @types/webpack -D`
 *
 * @type {import('webpack').Configuration}
 */
const config = {}
复制代码

webpack types

这里用的是 JSDoc 中的@type标记,它的语法作用是赋予后面一个单位以指定的 type 类型。

有关@type文档点这里

所谓的"配置文件自动提示"其实就是将./node_modules/@types/webpack/index.d.ts声明文件里的Configuration type 类型拿过来赋予到下面的config变量当中。

重点.jpg

这也意味着 JSDoc 拥有直接使用声明文件的能力。

JSDoc 的优雅使用方式

有的同学在使用 JSDoc 注释一个方法的时候,会写成类似这样:

/**
 * ajax 请求
 * @example `ajax('url', options)`
 * @param url 请求链接
 * @param options ajax控制参数
 *               .jsonp 登录模式。'common'常规模式;'silent'静默模式;'forced'强制模式;'backendSilent'后台静默
 *               .async 强制登录标志,会忽略当前登录态再跑一次登录 // TODO 待废弃
 *               .methods 跳过三秒内拒绝授权限制
 *               .success 是否忽略本地缓存的登录态,设为true会重新发起登录请求
 * @returns { Promise } 返回一个promise实例
 */
function ajaxOld (url, options) {}
复制代码

看注释似乎是说清楚了,但是在调用方法的过程中,IDE 并不会有友好交互的提示。

ajaxOld gif

看着 IDE 给出来的提示,甚至还会让我们不知道怎么去使用这个函数。

如果想顺着 IDE 的脾气,达成比较优雅的效果,可以去这么设计:

/**
 * ajax方法
 *
 * @example `ajax('url', options)`
 *
 * @typedef {Object} IAjaxOptions
 * @property {boolean} [jsonp] 是否jsonp
 * @property {boolean} [async] 是否async
 * @property {'GET'|'POST'} [methods] 请求方法
 * @property {(options: any)=>void} [success] 成功回调函数
 *
 * @param {string} url url
 * @param {IAjaxOptions} [options] 参数
 * @param { Promise }
 */
function ajaxNew (url, options) {}
复制代码

在 VSCode 里使用它的效果如下:

ajaxNew demo

使用过程中,IDE 对参数给出了比较友好提示。

这里有两个新的 tag,@typedef@property

其中@typedef的效果是声明一个 type 类型(或者理解为将一个类型起个别名),比如这里就是object类型,名为IAjaxOptions。 而@property的作用是声明上面类型里面包含的属性,用法和@param一致。

有关@typedef文档点这里

顺带一提,一些我们常用却忽略的库里面这种写法也比较多,比如这个html-webpack-plugin

html-webpack-plugin

有兴趣的同学可以去 github 上了解一下

与此相关的并且 VSCode 支持的 JSDoc 的 tag 有

  • params
  • typedef
  • property
  • return
  • this
  • constructor

更多例子可以在 JSDoc 的官网查到。

别瞎用

可能有的同学看了上面部分例子嗅到了一丝不安的气息。你的直觉很敏锐。用 JSDoc 标注的这种方法有一定的风险,不要瞎用。欲扬先抑,我们先聊聊瞎用它可能会导致的问题。

问题

  1. 全手动 通过上面的例子可以看出,这是一门"标记型"语法,手动挂档。极端情况你可能发现一半时间在写注释和声明。

  2. 不好维护,需要良好的团队规范或者项目文档去约束 有的人可能写多一点,有的人可能少写一点,还有的人可能增加了代码但不去增加 JSDoc 里的声明。

  3. 有引起智能提示作用域混乱的风险 在不开启静态类型检查的时候,IDE 会去完全接受你所设计的类型。要是只为了想要的提示而去强行指定 type 的话,别说是我告诉你这个方法的(跑)。

  4. 能力有限 最需要强调的是,在 VSCode 里,JSDoc 不是一个完美的类型补充工具。当你在实现一些复杂的类型时,可能会发现效果不尽人意,不要怀疑自己,很大程度是 VSCode 的锅。天知道我在研究它的时候度过了多少难忘的夜晚。

或许因为 JSDoc 更多是用来生成 api 文档的,供求关系不对等,因此 VSCode 对 JSDoc 的支持有限,在它的 Release Note 里只断断续续有一些更新。稍微列举下目前我遇到过的问题

  • 无法支持@private@protected这类 tag 修饰,表现在还是在提示中给了出来
  • 无法直接对某个函数定义函数重载,需要依靠对象的形式
  • 很多 tag 不支持,比如@augments@mixin等,官网给的例子不能在 VSCode 编辑器里展现出来预期的效果

好处

  1. 能力补全简单 相对改造成 .ts,你甚至都不用改文件拓展名。

  2. 对代码侵入性较低 可能你写的 300 行注释代码都活不过第一次打包。

  3. 不用担心 any script JS 代码里本身大批量是 any script 了,再怎么改都是进步。

这里提到的优点和改造 TS 过程中遇到的问题形成了鲜明对比。在你的团队里如果无法一下过渡到 TS,可以尝试一下使用 JSDoc。

改造建议

在对它的缺点和优点有了充分认识,在你决定在目前的代码中正式使用它之前,可以先参考以下建议,可能有助于你避免一些隐患:

优先考虑“治本”

VSCode 在 JS 环境里本身就有逻辑关联的能力。那么我们应该先考虑是否能够通过纠正某些逻辑,来让 IDE 自动识别出来它的关联。

举个栗子

var foo = {
    b: 2
}
foo.a = 1

foo.a // IDE 找不到 'a'
复制代码

可以改成

var foo = {
    a: 1,
    b: 2
}

foo.a // IDE 找到 foo.a
复制代码

或者

var foo = {}
foo.a = 1
foo.b = 2

foo.a // IDE 找到 foo.a
复制代码

更关注来自与 JS 的数据源

我们知道,在 VSCode 中,"ctrl + 单击",这个操作,可以跳到当前变量或属性的定义部分,后面我们简称这个行为为:「直跳」。

JSDoc 在 JS 中有一个非常好的优势。在和 TS 有关的能力中,「直跳」这个行为大部分时候会定位到代码的声明位置,而不是定义的位置。和 JS 打交道的程序员绝大部分不希望去关注目标代码的声明,而是想知道定义的内容是什么。如果在 JS 使用 type 全靠 .d.ts 声明文件,每次的「直跳」可能会使真相离得更远。

所以在实践的时候,应该善用typeof

var foo = {
    a: 2,
    b: 2
}

/**
 * @param {typeof foo} obj
 */
function bar (obj) {
    obj.a // 「直跳」找到 foo 里定义的 a
}
复制代码

打开 Check JS

虽然使用 JSDoc 语法带来的 type 能力很便利,但是也要讲究规范。很多时候虽然 IDE 是识别了,但是语法并不规范,最好开启 VSCode 设置里的Check JS选项,或者在代码顶部加上// @ts-check的注释。

比如下面这段代码:

var foo = {
    a: 2,
    b: 2
}

/** @type { typeof foo } */
var bar = {}

bar.a // ide可以关联到foo里面的a,但是bar并不包含这个属性
复制代码

这段代码是有明显 bug 的,但是因为手动赋予了 type,IDE 只能按照所写的东西去做。

当开启Check JS后,IDE 就会飘红提示:

demo

对应的关闭当前文件Check JS的顶部注释是// @ts-nocheck,忽略下一行 TS 错误是// @ts-ignore

本文前面的一些 gif demo 的顶部加了// @ts-nocheck,是为了避免 IDE 有飘红,干扰理解。

对使用频率高的对象加上 JSDoc type

如果注释太多,可能会影响阅读体验,而且你也不可能一次性把所有代码都改成优雅的 JSDoc。因此建议只对使用频率高的对象加上额外的 JSDoc 注释,比如zepto、全局变量、接口数据等。

有时我们在使用某个库的时候。会遇到变量名冲突,比如

var $ = requireFn('zepto')
复制代码

大部分场景下,前端团队自己实现的requireFn函数并不会返回zepto的 type。

这个时候,如果有zepto的 type 文件,可以这么操作:

/**
 * @typedef { typeof $ } ZeptoStatic // 把 $ 换个名字
 */
/** @type { ZeptoStatic } */ // 再赋予给 $
var $ = require('zepto')
复制代码

这样全部的$都有zepto的 type 能力了。

改用 JSDoc 去注释代码

我们经常会习惯性使用//去注释代码,但是如果还需要 IDE 提示的话,我们可以改为 JSDoc 的注释风格。当 VSCode 识别到 JSDoc 的时候,会给出你预设的提示:

// 这是bar
var bar = 1 // 这是也是bar
/** 这是foo */
var foo = 2

console.log(bar)
console.log(foo)
复制代码

把鼠标 hover 到console.log(xxx)后面的xxx上,观察下 IDE 的提示:

demo

demo

foo带上了我们写在 JSDoc 里的提示。

能换成 .ts,就换成 .ts

就像是我在《ts安利指南》里写到的。JS 改造成 TS 虽然麻烦,但是胜在安全,能力也更全面。因此有条件的话,还是应该改造成 TS 代码,毕竟长痛不如短痛。

至于怎么改造成 TS。这不是本文涉及的内容,就不做说明了,相关的优秀实践非常多,大家可以去搜一下。

进阶实践

前方列举两种更全面的实践场景,加深理解。

TS in JS with JSDoc

1.丢失出参类型

如标题所述。当入参经过一个方法出来之后,丢了类型,应该怎么优雅的建立联系:

const args = {
    a: 1,
    b: 2
}

const fn = {
    foo: function () {},
    bar: function () {}
}

/**
 * 合并两个作用域
 *
 * @returns { typeof args & typeof fn } // 这里用 returns 指定了返回类型
 */
function someMergeFn (args, fn) {
    // ...
}

const newObj = someMergeFn(args, fn)

newObj.a // 直跳到上面 args.a
newObj.bar // 直跳到上面 fn.bar
复制代码

gif demo

如动图所示,newObj.anewObj.bar建立了上文关联。

这里是通过 JSDoc 的 @returns tag 补全了出参的类型,使得调用函数后获得了指定的 type 类型。

2.类似require方法引入的包丢失类型

很多老代码里有自己实现加载其他包的方法,这里叫someRequireFn吧。但是也因为类似 1 的问题,丢了返回类型,这时可以通过以下方法找回来,并且拥有跨文件「直跳」的能力。

// otherService.js
;(function(global, factory){
    factory()
})(this, function () {
    var args = {
        a: 1,
        b: 2
    }
    var fn = {
        foo: function () {},
        bar: function () {}
    }

    /**
     * @returns { typeof args & typeof fn }
     */
    function someFactoryFn () {
        // ...
    }

    var newObj = someFactoryFn()

    return newObj

    module && (module.exports = newObj) // 这里是 TS 暴露模块的语法
})
复制代码
/**
 * @type { import ('./otherService') } // 这里用 type 指定 newObj 的类型
 */
var newObj = someRequireFn('./otherService')

newObj.a // 直跳到otherService.js的args.a
newObj.bar // 直跳到otherService.js的fn.bar
复制代码

gif demo

上面的otherService.js是直接执行函数,是为了模拟一些老代码常用的方式。需要注意的是,它需要有个暴露的出口,比如文中的

module && (module.exports = newObj)
复制代码

因为 TS 解析是全篇静态解析,因此不需要考虑module.exports在代码中的位置,不影响业务代码即可。

跨文件的「直跳」能力在回搠代码的时候非常方便,当你要参考当前函数干了啥的时候,不需要再通过全局搜索一个个过滤了。

3.更复杂的场景

如果 JSDoc 能力有限,可以直接借用更完备的 TS 能力去实现,再结合 JSDoc ,进行混合双打:

// InterfaceJs.d.ts
export interface INewObj {
    a: number
    b: number
    foo: () => void
    bar: () => void
}
复制代码
/**
 * @type { import ('./InterfaceJs.d').INewObj } // 手动引入外部的 .d.ts 声明文件
 */
var newObj = someRequireFn('./otherService')

newObj.a // 直跳到InterfaceJs.d.ts定义的args.a
newObj.bar // 直跳到InterfaceJs.d.ts定义的fn.bar
复制代码

gif demo

在 Vue 里使用 TS

让我们考察下在 Vue 里的实践。

在 Vue 里面使用 TS 有很多种方式。一种是通过 import 默认引入 Vue 的声明文件。一种是使用它的 class 风格。还有一种是最常见的,通过Vue的插件 Vetur 自动匹配它的声明文件。

这里针对这个插件多说几句。当你在script块中使用export default {}这个语法的时候,{}部分的 type 能力是插件帮忙关联到它的声明文件上的。

vue type

这里我们从另一个角度来考察它,不依靠插件能力,不多修改代码本身,并能使用官方提供的 TS 能力。

注:以下内容包含了泛型知识点,只需要应用的同学可以直接拉到结论部分。

Vue 之所以有 TS 的能力,是因为有vue.d.ts声明文件的存在,和其他几个文件一起,记录了 Vue 的所有能力。那么我们只要通过@type把它引进来,也就可以实现在 JS 里使用 Vue 的 type 能力了。

但是这里只完成了一半。想一下,new Vue()里面传递的是什么,一个对象。由于在vue.d.ts里面,传给Vueoptions是一个泛型对象,但我们并不能对一个对象使用泛型,因此需要一个helper的函数承接一下,内容很简单:

const vueOptionsTypeHelper = function (options) {
    return options
}
复制代码

接下来的工作就是对这个方法定义入参,通过对入参指定 type 就可以实现 TS 的能力。

找到vue.d.ts的代码,研究下new Vue的时候到底传的是什么东西。

export interface VueConstructor<V extends Vue = Vue> {
  new <Data = object, Methods = object, Computed = object, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): CombinedVueInstance<V, Data, Methods, Computed, Record<PropNames, any>>;
  new <Data = object, Methods = object, Computed = object, Props = object>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): CombinedVueInstance<V, Data, Methods, Computed, Record<keyof Props, any>>;
  new (options?: ComponentOptions<V>): CombinedVueInstance<V, object, object, object, Record<keyof object, any>>;
// ...
}
复制代码

通过阅读源码,找到了其中构造函数重载的三个形式,通过观察,第二个符合我们的需求。

其中options的 type 类型为

ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>
复制代码

把它的泛型和依赖关系取出来,新建一个名为vue.helper.d.ts的声明文件,内容如下:

// vue.helper.d.ts
import Vue from 'vue'
import { ThisTypedComponentOptionsWithRecordProps } from 'vue/types/options'

export type VueComponentOptions = <
  Data = object,
  Methods = object,
  Computed = object,
  Props = object
  >(options?: ThisTypedComponentOptionsWithRecordProps<Vue, Data, Methods, Computed, Props>)
  => object
复制代码

改造vueOptionsTypeHelper方法为

/**
 * @type { import ('./vue.helper').VueComponentOptions }
 */
const vueOptionsTypeHelper = function (options) {
    return options
}
复制代码

然后用vueOptionsTypeHelper包裹住options,比如

const options = vueOptionsTypeHelper({
    name: 'app',
    components: {
        App,
    },
    data () {
        return {
            shareDialog: false,
            inWxapp: false,
        }
    },
    methods: {
        methods1 () {
            return true
        },
        methods2 () {
            return false
        },
        onclick () {
            console.log(123)
        }
    },
    created () {
        
    },
    mounted () {
        this.methods1()
    },
})
复制代码

这样,指定的object就有了Vue的 type 提示能力。

gif demo

你可能想问,既然Vue已经有比较完善的在 JS 里使用 type 能力的实践,那这个有什么用呢?

你可以想一想,自己团队是不是也有类似Vue的框架?你们团队的框架离智能提示只差一份声明文件的距离(小声bb:而这份声明文件的编写可能得花上整个过程的95%的时间)。

类似的场景非常多,大家可以发挥下想象力,这里不多赘述。

总结

内容比较多,这里来个小结。

在 JS 里使用 TS 能力的方法

  1. 使用声明文件
  2. 使用 JSDoc

这两种方式还可以一起作用,实现一些复杂的类型效果。

怎么去应用

  1. 对公共组件和全局变量编写声明文件
  2. 对自定义的函数编写 JSDoc 注释,并优雅的完善它
  3. 对作用域不清晰的的变量、对象等使用 JSDoc 的@type,去指定它的类型

注意事项

  1. 不要瞎用 JSDoc
  2. 尽量让代码「直跳」到它定义的位置

开头提了一下,后面再次提一下:需要直接体验 demo 的同学可以点这里,拉下来后在本地用 VSCode 体验一下。

写在最后

这篇文章拿给身边一些朋友看的时候,普遍反馈了对于 TS 的疲惫。也许是受伤太深,也许是被周围的反对声压折了放在键盘上的双手,也许是只为了体验生活写写代码,没必要和自己过不去。

我之所以喜欢 TS,就是被它的自动提示所吸引(又是静态类型语言玩剩的东西)。在深入了解之后,TS 的功能甚至弥补了我自身的一些缺点,比如粗心。深切的感受才会有深刻的觉悟。虽然写起来是比较麻烦了,但是我对它的评价高于这些不便,因此我会去使用它。

退一万步说,在你去面试的时候人家问你 TS 的东西,你还可以拿这些去忽悠他。

乖巧.jpg


如果你觉得这篇内容对你有价值,欢迎点赞并关注我们前端团队的官网和我们的微信公众号(WecTeam),每周都有优质文章推送:

WecTeam