从一个小小的arrify工具库的源码学习如何为社区贡献开源代码

102 阅读8分钟

这是学习若川文章的笔记和感想, 希望对你也有用.

真没有想到这个么简单的库, 一周的下载量竟然有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

分配律的条件:

  1. 参数是泛型类型
  2. 代入参数的是联合类型

要注意特殊的 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 用于测试错误使用

总结

从这个小小的代码库, 写到了如何测试代码和测试声明文件以及善于抽离公共函数的思想.

引用文章:

# TS关键字extends用法总结

# TypeScript 学习笔记(四)- extends 的作用