不知道有多少 TS 爱好者哀叹过这个问题:虽然我很想用 TS,奈何老大只让用 JS。今天我,告诉你,在 JavaScript 中也可以很流畅的使用 TypeScript ,而且你的老大不会找你的麻烦。
写在前面
往期文章:《ts安利指南》
很多同学在看了《ts安利指南》后,评论说道:“TS 虽然香,奈何我们老大没兴趣。” “有兴趣,但是需要时间慢慢推行,没办法啊。” 当时我就觉得,那我安利的不到位啊,于是做了很长时间的准备,写出了这篇续集,讲述了我在 JS 里面苦苦探索 TS 实践的结果。
如果你有 TS 基础,读起来会很顺畅。如果没有 TS 基础,也能看懂一些(这也是我的目标),但是如果有会理解的更深一些。如果你不喜欢 TS,emmmmmm…就当看个乐吧 😛
本文是针对最新版的 VSCode(v1.41.0) 下所写的。其他 IDE 如果有出入,以 IDE 官方能力为准。
WebStorm 也支持,但是因为它包含了自家特色的智能引擎,隔壁行八竿子打不到的代码也能在当前作用域找到,所以这种神器在这里不做讨论。
什么,你用记事本?
友情提示:配合 VSCode 阅读该文时,体验更佳。主依赖环境只有最新版的 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 就会自动弹出它里面的方法。
明明属于 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 声明文件
看这张图是不是有很多熟悉的方法的名字?它提供了 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 的语法提示。
由于 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
这里有一点需要注意的是,IDE 会以声明文件定义的 Api type 优先。如果声明文件里面没有包含对应 .js 文件的某个暴露的方法,IDE 也不会给出存在这个方法的提示,甚至在开启语法检查的时候还会报错。
另外 VSCode 引入声明文件有自己的一套顺序规则,类似 node 的 require 规则。太长了就不放出来了,有兴趣的同学可以点这里了解详情。
指定默认入口:
包内自带的声明文件还可以不和源码放一起,单独放在某个文件夹维护,只要在 package.json 中指定声明文件的入口,VSCode 就会自动去找这个文件。比如 Vue 的 package.json 里面
在typings
或者types
的属性下指定入口,当我们直接import Vue from 'vue'
的时候,就会去寻找指定的声明文件。
来自 @types 的声明文件
还有一些包他们并不把声明文件放在自己的目录下面,而是托管在一个专门放声明文件的私有域下。私有域里最有名的包可能就是 @types/node 了。
如果你想使用 node 的声明文件,你需要安装 @types/node。
npm i @types/node -D
这样使用 node.js 的 Api 就有了智能提示。
有时候你可能会发现没有安装@types/node
,IDE 也会提示 node.js 的 api。那是因为 VSCode 默认还会在几个全局的地方找 types 包,而你正好有,那就顺理成章有了提示。
你可能会好奇这个@types
是个什么域。答案是:Microsoft 的域。符合规范的的声明文件可以发布到这个地方,供全世界的程序猿/媛使用。如果你想拥有某个 node_modules 的提示,但是包里面并没有提供声明文件,可以去这里找找。
当然你也可以按照规范写一个声明文件发布上去供其他人使用,点击这里去按要求提个 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"
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 // 定义全局变量
这里涉及到了声明类型暴露方式的知识点,有兴趣更深入了解的同学可以点这里去深造。
神奇的注释
说到在 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
表示该方法接收一个名为name
的string
类型的参数。
在支持 JSDoc 的 IDE 当中,比如 VSCode 和 WebStorm,在使用这个方法 hover 的时候还会给出比较友好的提示。
你可能不知道的 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
,并且把入参都返回了出来。我们来看下他们返回值是什么类型:
可以看到,拥有@param
的foo
函数返回出来的参数也是 string 类型,而bar
函数返回的是 any。
JSDoc 为函数补充了入参类型,使得 IDE 能够识别出来函数入参出参的关系。
另外一方面,在函数内部,有了入参类型的foo
函数的参数name
,IDE 给了它提示string
内方法的能力。
这就是 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
时,会得到firstname
和nameLength
两个属性。
还记得在《ts安利指南》中提到过的"配置文件自动提示"吗?
/**
* webpack配置自动提示
*
* 先安装对应的types包: `npm i @types/webpack -D`
*
* @type {import('webpack').Configuration}
*/
const config = {}
这里用的是 JSDoc 中的@type
标记,它的语法作用是赋予后面一个单位以指定的 type 类型。
有关@type
的文档点这里
所谓的"配置文件自动提示"其实就是将./node_modules/@types/webpack/index.d.ts
声明文件里的Configuration
type 类型拿过来赋予到下面的config
变量当中。
这也意味着 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 并不会有友好交互的提示。
看着 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 里使用它的效果如下:
使用过程中,IDE 对参数给出了比较友好提示。
这里有两个新的 tag,@typedef
和@property
。
其中@typedef
的效果是声明一个 type 类型(或者理解为将一个类型起个别名),比如这里就是object
类型,名为IAjaxOptions
。
而@property
的作用是声明上面类型里面包含的属性,用法和@param
一致。
有关@typedef
的文档点这里
顺带一提,一些我们常用却忽略的库里面这种写法也比较多,比如这个html-webpack-plugin
有兴趣的同学可以去 github 上了解一下
与此相关的并且 VSCode 支持的 JSDoc 的 tag 有
params
typedef
property
return
this
constructor
更多例子可以在 JSDoc 的官网查到。
别瞎用
可能有的同学看了上面部分例子嗅到了一丝不安的气息。你的直觉很敏锐。用 JSDoc 标注的这种方法有一定的风险,不要瞎用。欲扬先抑,我们先聊聊瞎用它可能会导致的问题。
问题:
-
全手动
通过上面的例子可以看出,这是一门"标记型"语法,手动挂档。极端情况你可能发现一半时间在写注释和声明。 -
不好维护,需要良好的团队规范或者项目文档去约束
有的人可能写多一点,有的人可能少写一点,还有的人可能增加了代码但不去增加 JSDoc 里的声明。 -
有引起智能提示作用域混乱的风险
在不开启静态类型检查的时候,IDE 会去完全接受你所设计的类型。要是只为了想要的提示而去强行指定 type 的话,别说是我告诉你这个方法的(跑)。 -
能力有限
最需要强调的是,在 VSCode 里,JSDoc 不是一个完美的类型补充工具。当你在实现一些复杂的类型时,可能会发现效果不尽人意,不要怀疑自己,很大程度是 VSCode 的锅。天知道我在研究它的时候度过了多少难忘的夜晚。
或许因为 JSDoc 更多是用来生成 api 文档的,供求关系不对等,因此 VSCode 对 JSDoc 的支持有限,在它的 Release Note 里只断断续续有一些更新。稍微列举下目前我遇到过的问题
- 无法支持
@private
、@protected
这类 tag 修饰,表现在还是在提示中给了出来 - 无法直接对某个函数定义函数重载,需要依靠对象的形式
- 很多 tag 不支持,比如
@augments
、@mixin
等,官网给的例子不能在 VSCode 编辑器里展现出来预期的效果
好处:
-
能力补全简单
相对改造成 .ts,你甚至都不用改文件拓展名。 -
对代码侵入性较低
可能你写的 300 行注释代码都活不过第一次打包。 -
不用担心 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 就会飘红提示:
对应的关闭当前文件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 的提示:
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
如动图所示,newObj.a
和newObj.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
上面的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
在 Vue 里使用 TS
让我们考察下在 Vue 里的实践。
在 Vue 里面使用 TS 有很多种方式。一种是通过 import
默认引入 Vue
的声明文件。一种是使用它的 class 风格。还有一种是最常见的,通过Vue
的插件 Vetur 自动匹配它的声明文件。
这里针对这个插件多说几句。当你在script
块中使用export default {}
这个语法的时候,{}
部分的 type 能力是插件帮忙关联到它的声明文件上的。
这里我们从另一个角度来考察它,不依靠插件能力,不多修改代码本身,并能使用官方提供的 TS 能力。
注:以下内容包含了泛型知识点,只需要应用的同学可以直接拉到结论部分。
Vue 之所以有 TS 的能力,是因为有vue.d.ts
声明文件的存在,和其他几个文件一起,记录了 Vue 的所有能力。那么我们只要通过@type
把它引进来,也就可以实现在 JS 里使用 Vue 的 type 能力了。
但是这里只完成了一半。想一下,new Vue()
里面传递的是什么,一个对象。由于在vue.d.ts
里面,传给Vue
的options
是一个泛型对象,但我们并不能对一个对象使用泛型,因此需要一个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 提示能力。
你可能想问,既然Vue
已经有比较完善的在 JS 里使用 type 能力的实践,那这个有什么用呢?
你可以想一想,自己团队是不是也有类似Vue
的框架?你们团队的框架离智能提示只差一份声明文件的距离(小声bb:而这份声明文件的编写可能得花上整个过程的95%的时间)。
类似的场景非常多,大家可以发挥下想象力,这里不多赘述。
总结
内容比较多,这里来个小结。
在 JS 里使用 TS 能力的方法
- 使用声明文件
- 使用 JSDoc
这两种方式还可以一起作用,实现一些复杂的类型效果。
怎么去应用
- 对公共组件和全局变量编写声明文件
- 对自定义的函数编写 JSDoc 注释,并优雅的完善它
- 对作用域不清晰的的变量、对象等使用 JSDoc 的
@type
,去指定它的类型
注意事项
- 不要瞎用 JSDoc
- 尽量让代码「直跳」到它定义的位置
开头提了一下,后面再次提一下:需要直接体验 demo 的同学可以点这里,拉下来后在本地用 VSCode 体验一下。
写在最后
这篇文章拿给身边一些朋友看的时候,普遍反馈了对于 TS 的疲惫。也许是受伤太深,也许是被周围的反对声压折了放在键盘上的双手,也许是只为了体验生活写写代码,没必要和自己过不去。
我之所以喜欢 TS,就是被它的自动提示所吸引(又是静态类型语言玩剩的东西)。在深入了解之后,TS 的功能甚至弥补了我自身的一些缺点,比如粗心。深切的感受才会有深刻的觉悟。虽然写起来是比较麻烦了,但是我对它的评价高于这些不便,因此我会去使用它。
退一万步说,在你去面试的时候人家问你 TS 的东西,你还可以拿这些去忽悠他。
如果你觉得这篇内容对你有价值,欢迎点赞并关注我们前端团队的官网和我们的微信公众号(WecTeam),每周都有优质文章推送: