- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第33期,链接:juejin.cn/post/710021…
这是学习若川文章的笔记和感想, 希望对你也有用.
真没有想到这个么简单的库, 一周的下载量竟然有2196万. 惊奇的同时又感到作为工具使用者的痛苦和困惑. 这么个功能都有个库, 而且这么多人使用, 那实现项目中要用到多少类似的库, 要用多少时间来在NPM库的汪洋大海中苦苦搜寻. 这个库的作用和增加的心智负担到底能否平衡啊! 哎, 太难了!
这个库的github地址为: github.com/sindresorhu… 为了快速像vscode查看代码, 我们可以在网址的github后面加个1s, 即:github1s.com/sindresorhu… 就可以快速用在线vscode加载整个仓库代码. 非常方便. 记住了仅加个1s就可以了.
分析下 Package.json 文件
{
...
"type": "module",
"exports": "./index.js",
...
"scripts": {
"test": "xo && ava && tsd"
},
"files": [
"index.js",
"index.d.ts"
],
"keywords": [
"array",
"arrify",
"arrayify",
"convert",
"value",
"ensure"
],
"devDependencies": {
"ava": "^3.15.0",
"tsd": "^0.14.0",
"xo": "^0.39.1"
},
...
}
-
"type": "module"
表示这个库为module
模式即 ESM 规范使用. -
"exports": "./index.js"
指定对外输出的文件入口 -
"scripts": { "test": "xo && ava && tsd" },
表示依次执行这三个插件来保证这个库符合设计目标.-
xo: 这个库有 6.6k star, 最初是基于 ESLint 的。一开始只是共享 ESLint 配置,但很快就不再是了。它更简单了, 只要输入 xo 就行了。不需要决策。不需要配置。但是,您仍然可以在直接使用 ESLint 和 ESLint可共享配置 时获得 XO 的大部分好处。它是一个固执己见但可配置的 ESLint 包装器,包含很多好东西。强制执行严格且可读的代码。永远不要再讨论拉请求的代码格式了!它没有
.eslintrc
管理, 也能很好的工作! -
ava: 这个库有 19.8k star,AVA 是 Node.js 的测试运行程序,它提供了简洁的 API、详细的错误输出、包含新的语言特性和进程隔离,让你可以自信地开发. 这里有中文翻译版本,具体翻译课查看 github.com/avajs/ava-d… ,它最大的优势应该就是可以并行的执行测试用例,这对于IO繁重的测试就特别的友好了。
-
tsd: 这个库有 1.4k star, 此工具允许你通过创建
.test-d.ts
扩展名的文件为类型定义编写测试(即: 你的xx.d.ts
文件)。 这些.test-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];
}
- 判断 null 或者undefined,则返回空的数组
- 判断传入的是一个数组的话,则返回它本身
- 判断传入的类型为string字符串的话,则返回一个以这个字符串为唯一元素的数组
if (typeof value[Symbol.iterator] === 'function') { return [...value]; }
这句是如果有迭代器属性且为函数时返回这个对象的解构数组. 后面细说下这个东西.- 以上都不符合就直接返回这个入参为元素的数组.
学习下 Symbol.iterator
阮一峰老师有篇关于 iterator 文章.
一个数据结构只要具有 Symbol.iterator
属性,就可认为是“可遍历的”(iterable)。Symbol.iterator 属性是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名 Symbol.iterator,它是一个返回Symbol对象的属性。要注意的是: String, 函数入参arguments, dom对象列表NodeList 都是可迭代的.
学习下 typescript 声明文件 index.d.ts
在学习这个之前先学习下extends这关键词的作用. 看了一篇关于 extends 的文章, 感觉说的很好. 总结说来 extends 有以下几个作用:
- 用于继承: 接口继承和类继承
- 用于条件判断: 条件判断就是用来判断一个类型是不是可以分配给另一个类型
type Animal = {
name: string;
}
type Dog = {
name: string;
action: string;
}
type Bool1 = Animal extends Dog ? 'yes' : 'no'; // 'no'
type Bool2 = Dog extends Animal ? 'yes' : 'no'; // 'yes'
需要理解的是,这里 Dog extends Animal
,是指类型 Dog 可以分配给类型 Animal,而不是说类型 Dog 是类型 Animal 的子集。因为 Animal 和 Dog 的类型完全相同,或者说 Animal 类型的一切约束条件,Dog 都具备, 所以表达式为 true , 所以Bool2 结果为 'yes'
. 而对于 Animal extends Dog
, 由于 Animal 没有类型为string的action属性,类型Animal不满足类型Dog的类型约束。所以表达式为 false, 所以 Bool2 为 'no'
.
- 用于泛型条件判断
type A1 = 'x' extends 'x' ? string : number; // string
type A2 = 'x' | 'y' extends 'x' ? string : number; // number
type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'> // string | number
P是带参数T的泛型类型,其表达式和A1,A2的形式完全相同,A3是泛型类型P传入参数’x’ | 'y’得到的类型,A3和A2的类型相似,但是结果不同,出现这个结果的原因是所谓的分配条件类型。
对于使用extends关键字的条件类型(即上面的三元表达式类型),如果extends前面的参数是一个泛型类型,当传入该参数的是联合类型,则使用分配律计算最终的结果。分配律是指,将联合类型的联合项拆成单项,分别代入条件类型,然后将每个单项代入得到的结果再联合起来,得到最终的判断结果。
type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'> // string | number
上面的表达式可以拆分为:
P<'x' | 'y'> => P<'x'> | P<'y'>
P<'x'> = 'x' extends 'x' ? string : number => string
P<'y'> = 'y' extends 'x' ? string : number => number
P<'x' | 'y'> = P<'x'> | P<'y'> = string | number
分配律的条件:
- 参数是泛型类型
- 代入参数的是联合类型
要注意特殊的 Never
type A1 = never extends 'x' ? string : number; // string
type P<T> = T extends 'x' ? string : number;
type A2 = P<never> // never
never是所有类型的子类型, 所以A1的表达式为true(感觉还是有点懵, 先强记一下吧) 至于A2为never是因为在泛型条件判断里, never被认为是空的联合类型,也就是说,没有联合项的联合类型,所以还是满足上面的分配律,然而因为没有联合项可以分配,所以P的表达式其实根本就没有执行,所以A2的定义也就类似于永远没有返回的函数一样,是never类型的。下面的例子就不一样了.
type P<T> = [T] extends ['x'] ? string : number;
type A1 = P<'x' | 'y'> // number
type A2 = P<never> // string
当泛型参数被[]括起来后, 就阻止了分配律, 此时参数会被当做一个整体.
- 用于泛型约束
class Student {
constructor(private info: Person) {}
getInfo<T extends keyof Person>(key: T): Person[T] {
return this.info[key];
}
}
这里extends对传入的参数作了一个限制,参数必须是 Person的成员名的联合类型,避免传入其他key。
下面开始看源码的声明文件:
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];
乍一看好复杂, 其实我们一点点的分析下, 也就没有那么唬人了. 分析如下:
ValueType extends (null | undefined)
意思是ValueType是null或undefined就为[], 否则往下判断ValueType extends string
是 string 就为 [string], 否则往下ValueType extends readonly unknown[]
是只读类型的数组, 就原样返回, 否则往下ValueType extends Iterable<infer T>
是迭代器类型, 就返回迭代器的元素组成的数组类型, 否则就返回参数组成的数组类型
学习下测试用例文件
// 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);
});
通过这个文件我们可以学习下如果给代码写测试, 以保证我们的代码在以后的变动中不会影响使用者的使用. 这个测试蛮简单的, 必定这个库就是挺简单.
下面简单看下测试声明文件的方法:
/* 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(["🦄"]));
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写内部的一个工具库.
expectType<期望的类型>(arrify(参数))
这个是精确类型匹配- 如果仍然希望使用松散类型断言,可以使用
expectAssignable
expectError
用于测试错误使用
总结
从这个小小的代码库, 写到了如何测试代码和测试声明文件以及善于抽离公共函数的思想.
引用文章: