将一个值转换为一个数组
前言
这是源码共读的第 33 期,链接:https://juejin.cn/post/7100218384918249503。
arrify 源码解读
前菜
首先打开 package.json 看看
{
"type": "module",
"exports": "./index.js",
"engines": {
"node": ">=12"
},
"scripts": {
"test": "xo && ava && tsd"
},
"files": ["index.js", "index.d.ts"],
"devDependencies": {
"ava": "^3.15.0",
"tsd": "^0.14.0",
"xo": "^0.39.1"
}
}
"type"
字段定义了 Node.js 用于以该package.json
文件作为最近父级的所有.js
文件的模块格式。"exports"
字段提供了一种方法来为不同环境和 javascript 风格公开包模块,Node.js 12+ 支持它作为"main"
的替代方案。"engines"
字段可以指定运行该包模块所需的 Node.js 版本(这里定为 12+ 应该是为了让 Node.js 能够识别exports
字段)以及能够正确安装该包模块的 npm 版本。"files"
字段描述了当该包模块作为依赖项安装时要包含的条目。
除此之外还有三个开发依赖:
- xo:JavaScript/TypeScript linter(ESLint 包装器)具有很好的默认值
- tsd:该工具允许您通过创建扩展名为
.test-d.ts
的文件来为您的类型定义(即您的.d.ts
文件)编写测试。 - ava:测试工具,用于测试模块的功能。运行后会执行根目录中的
test.js
的文件。
和一个测试启动脚本:test: "xo && ava && tsd"
- 先执行 xo 对源码进行检查
- 再执行 ava 跑
test.js
测试 index.js - 最后执行 tsd 跑
index.test-d.ts
测试 index.d.ts
主菜
接下来就到了我们的主菜 index.js
:
export default function arrify(value) {
if (value === null || value === undefined) {
return []
}
if (Array.isArray(value)) {
return value
}
if (typeof value === 'string') {
return [value]
}
if (typeof value[Symbol.iterator] === 'function') {
return [...value]
}
return [value]
}
从这段源码中我们可以看到,arrify
函数的转换规则是:
- 如果
value
为null
或undefined
,则返回空数组 - 如果
value
为数组,则返回该数组 - 如果
value
为字符串,则返回一个包含该字符串的数组 - 如果
value
为可遍历对象,则返回一个浅拷贝的包含该对象迭代结果的数组 - 否则返回一个包含该值的数组
另外,关于如 arguments
对象的类数组对象,作者在 https://github.com/sindresorhus/arrify/issues/2 中解释了不对其进行转换的原因:
APIs should IMHO just accept Arrays. If you need to use an array like object it should be up to you to convert it. What if a user actually want the array-like object to be an element in the array. With ES6 this will be easy with
Array.from()
.
谷歌翻译了下,大意是:
“就我来说,API 应该只接受数组。如果您需要使用类似对象的数组,则应由您来转换它。如果用户实际上希望类数组对象成为数组中的一个元素怎么办。在 ES6 中,使用 Array.from() 会很容易。”
荤素搭配
看完函数实现,我们再来看看如何搭配 ts 类型定义:
// index.d.ts
export default function arrify<ValueType>(value: ValueType): ValueType extends null | undefined
? [] // eslint-disable-line @typescript-eslint/ban-types
: ValueType extends string
? [string]
: ValueType extends readonly unknown[]
? ValueType
: ValueType extends Iterable<infer T>
? T[]
: [ValueType]
可以看出,类型定义方式和上面 arrify
函数的实现方式是一致的:
- 如果泛型
ValueType
为null | undefined
联合类型的子类,则返回空数组类型 - 如果泛型
ValueType
为字符串类型的子类,则返回一个字符串数组类型 - 如果泛型
ValueType
为数组类型的子类,则返回该数组类型 - 如果泛型
ValueType
为可遍历对象类型的子类,则返回一个包含该对象迭代结果的数组类型 - 否则返回一个包含该值的数组类型
餐后甜点
到这里正餐已经结束,但我们还可以品尝一些餐后甜点。
还记得那条测试脚本吗:test: "xo && ava && tsd"
xo 使用默认的配置,暂时不需要关心。ava 和 tsd 则需要我们添加测试脚本。
先来看功能测试的脚本 test.js
:
import test from 'ava'
import arrify from './index.js'
test('main', (t) => {
t.deepEqual(arrify('foo'), ['foo'])
t.deepEqual(
arrify(
new Map([
[1, 2],
['a', 'b'],
]),
),
[
[1, 2],
['a', 'b'],
],
)
t.deepEqual(arrify(new Set([1, 2])), [1, 2])
t.deepEqual(arrify(null), [])
t.deepEqual(arrify(undefined), [])
const fooArray = ['foo']
t.is(arrify(fooArray), fooArray)
})
如作者所说,涵盖了除类数组外的常见情况。
然后是类型测试的脚本 index.test-d.ts
:
/* eslint-disable @typescript-eslint/ban-types */
import { expectType, expectError, expectAssignable } from 'tsd'
import arrify from './index.js'
expectType<[]>(arrify(null))
expectType<[]>(arrify(undefined))
expectType<[string]>(arrify('🦄'))
expectType<string[]>(arrify(['🦄']))
expectAssignable<[boolean]>(arrify(true))
expectType<[number]>(arrify(1))
expectAssignable<[Record<string, unknown>]>(arrify({}))
expectType<[number, string]>(arrify([1, 'foo']))
expectType<Array<string | boolean>>(arrify(new Set<string | boolean>(['🦄', true])))
expectType<number[]>(arrify(new Set([1, 2])))
expectError(arrify(['🦄'] as const).push(''))
expectType<[number, number] | []>(arrify(false ? [1, 2] : null))
expectType<[number, number] | []>(arrify(false ? [1, 2] : undefined))
expectType<[number, number] | [string]>(arrify(false ? [1, 2] : '🦄'))
expectType<[number, number] | [string]>(arrify(false ? [1, 2] : ['🦄']))
expectAssignable<number[] | [boolean]>(arrify(false ? [1, 2] : true))
expectAssignable<number[] | [number]>(arrify(false ? [1, 2] : 3))
expectAssignable<number[] | [Record<string, unknown>]>(arrify(false ? [1, 2] : {}))
expectAssignable<number[] | [number, string]>(arrify(false ? [1, 2] : [1, 'foo']))
expectAssignable<number[] | Array<string | boolean>>(
arrify(false ? [1, 2] : new Set<string | boolean>(['🦄', true])),
)
expectAssignable<number[] | [boolean] | [string]>(arrify(false ? [1, 2] : false ? true : '🦄'))
这类型测试的量比功能测试的多了不少啊,自从用了 ts 以后,还真是开发五分钟,定义两小时 😂
吃饱后的闲聊
花了不少时间看明白 package.json
里的字段,以后再看其他包的时候好歹能看得懂这几个了。每次认识一两个,积少成多。
功能实现没几行代码,但是类型定义和测试的代码量估计得占了有 4/5,不得不说是真的稳,想写出个 bug 都不容易。
不过我除了将 arguments 对象转成数组外,也想不出这玩意儿的其他应用场景了。看了下依赖该包的包,数量有 900 多个,里面就有刚才提到的 xo 和 ava。好吧,看来应用还是很广泛的。