@cult-frog/primitives 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/callbacks/constant.d.ts +21 -0
- package/dist/callbacks/constant.d.ts.map +1 -0
- package/dist/callbacks/constant.js +23 -0
- package/dist/callbacks/constant.js.map +1 -0
- package/dist/callbacks/identity.d.ts +20 -0
- package/dist/callbacks/identity.d.ts.map +1 -0
- package/dist/callbacks/identity.js +22 -0
- package/dist/callbacks/identity.js.map +1 -0
- package/dist/callbacks/index.d.ts +5 -0
- package/dist/callbacks/index.d.ts.map +1 -0
- package/dist/callbacks/index.js +5 -0
- package/dist/callbacks/index.js.map +1 -0
- package/dist/callbacks/noop.d.ts +16 -0
- package/dist/callbacks/noop.d.ts.map +1 -0
- package/dist/callbacks/noop.js +18 -0
- package/dist/callbacks/noop.js.map +1 -0
- package/dist/callbacks/once.d.ts +28 -0
- package/dist/callbacks/once.d.ts.map +1 -0
- package/dist/callbacks/once.js +38 -0
- package/dist/callbacks/once.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/predicates/index.d.ts +7 -0
- package/dist/predicates/index.d.ts.map +1 -0
- package/dist/predicates/index.js +7 -0
- package/dist/predicates/index.js.map +1 -0
- package/dist/predicates/nil.d.ts +31 -0
- package/dist/predicates/nil.d.ts.map +1 -0
- package/dist/predicates/nil.js +35 -0
- package/dist/predicates/nil.js.map +1 -0
- package/dist/predicates/null.d.ts +29 -0
- package/dist/predicates/null.d.ts.map +1 -0
- package/dist/predicates/null.js +33 -0
- package/dist/predicates/null.js.map +1 -0
- package/dist/predicates/number.d.ts +20 -0
- package/dist/predicates/number.d.ts.map +1 -0
- package/dist/predicates/number.js +22 -0
- package/dist/predicates/number.js.map +1 -0
- package/dist/predicates/record.d.ts +59 -0
- package/dist/predicates/record.d.ts.map +1 -0
- package/dist/predicates/record.js +69 -0
- package/dist/predicates/record.js.map +1 -0
- package/dist/predicates/string.d.ts +15 -0
- package/dist/predicates/string.d.ts.map +1 -0
- package/dist/predicates/string.js +17 -0
- package/dist/predicates/string.js.map +1 -0
- package/dist/predicates/undefined.d.ts +29 -0
- package/dist/predicates/undefined.d.ts.map +1 -0
- package/dist/predicates/undefined.js +33 -0
- package/dist/predicates/undefined.js.map +1 -0
- package/package.json +68 -0
- package/src/callbacks/constant.test.ts +31 -0
- package/src/callbacks/constant.ts +22 -0
- package/src/callbacks/identity.test.ts +24 -0
- package/src/callbacks/identity.ts +21 -0
- package/src/callbacks/index.ts +4 -0
- package/src/callbacks/noop.test.ts +19 -0
- package/src/callbacks/noop.ts +17 -0
- package/src/callbacks/once.test.ts +93 -0
- package/src/callbacks/once.ts +41 -0
- package/src/index.ts +2 -0
- package/src/predicates/index.ts +6 -0
- package/src/predicates/nil.test.ts +43 -0
- package/src/predicates/nil.ts +35 -0
- package/src/predicates/null.test.ts +42 -0
- package/src/predicates/null.ts +33 -0
- package/src/predicates/number.test.ts +38 -0
- package/src/predicates/number.ts +21 -0
- package/src/predicates/record.test.ts +107 -0
- package/src/predicates/record.ts +74 -0
- package/src/predicates/string.test.ts +25 -0
- package/src/predicates/string.ts +16 -0
- package/src/predicates/undefined.test.ts +42 -0
- package/src/predicates/undefined.ts +33 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/predicates/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAO9E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,IAAI,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAE1F;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,WAAW,EAC/C,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,CAAC,GACL,KAAK,IAAI,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAE7B"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a value is a plain object, i.e. an object literal or an object created with
|
|
3
|
+
* `Object.create(null)`.
|
|
4
|
+
*
|
|
5
|
+
* Arrays, class instances and built-ins such as `Date`, `Map` and `RegExp` are rejected. This
|
|
6
|
+
* makes it suitable for validating parsed data such as JSON before reading its properties.
|
|
7
|
+
*
|
|
8
|
+
* Objects created in another realm (an iframe or a Node `vm` context) are also rejected, because
|
|
9
|
+
* their prototype is a different `Object.prototype`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* isRecord({ id: 1 }); // true
|
|
14
|
+
* isRecord(Object.create(null)); // true
|
|
15
|
+
* isRecord([]); // false
|
|
16
|
+
* isRecord(new Date()); // false
|
|
17
|
+
* isRecord(null); // false
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @param value - The value to check.
|
|
21
|
+
* @returns `true` if `value` is a plain object, otherwise `false`.
|
|
22
|
+
*/
|
|
23
|
+
export function isRecord(value) {
|
|
24
|
+
if (typeof value !== 'object' || value === null) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const prototype = Object.getPrototypeOf(value);
|
|
28
|
+
return prototype === Object.prototype || prototype === null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Checks if a value is not a plain object. This is the inverse of {@link isRecord}.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* isNotRecord(new Date()); // true
|
|
36
|
+
* isNotRecord([]); // true
|
|
37
|
+
* isNotRecord({ id: 1 }); // false
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @param value - The value to check.
|
|
41
|
+
* @returns `true` if `value` is not a plain object, otherwise `false`.
|
|
42
|
+
*/
|
|
43
|
+
export function isNotRecord(value) {
|
|
44
|
+
return !isRecord(value);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Checks if an object has the given key as its own property, narrowing the object so the
|
|
48
|
+
* property can be read.
|
|
49
|
+
*
|
|
50
|
+
* Inherited properties are not counted, so keys such as `toString` do not match. The property
|
|
51
|
+
* may still hold `undefined`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const data: unknown = JSON.parse(input);
|
|
56
|
+
*
|
|
57
|
+
* if (isRecord(data) && hasProperty(data, 'id') && isNumber(data.id)) {
|
|
58
|
+
* data.id; // number
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @param value - The object to check.
|
|
63
|
+
* @param key - The property key to look for.
|
|
64
|
+
* @returns `true` if `value` has `key` as an own property, otherwise `false`.
|
|
65
|
+
*/
|
|
66
|
+
export function hasProperty(value, key) {
|
|
67
|
+
return Object.hasOwn(value, key);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../../src/predicates/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,SAAS,GAAY,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IACxD,OAAO,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAI,KAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,WAAW,CACzB,KAAa,EACb,GAAM;IAEN,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a value is a string primitive. `String` wrapper objects are rejected.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* isString('hello'); // true
|
|
7
|
+
* isString(''); // true
|
|
8
|
+
* isString(42); // false
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* @param value - The value to check.
|
|
12
|
+
* @returns `true` if `value` is a string, otherwise `false`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isString(value: unknown): value is string;
|
|
15
|
+
//# sourceMappingURL=string.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"string.d.ts","sourceRoot":"","sources":["../../src/predicates/string.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAExD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a value is a string primitive. `String` wrapper objects are rejected.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* isString('hello'); // true
|
|
7
|
+
* isString(''); // true
|
|
8
|
+
* isString(42); // false
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* @param value - The value to check.
|
|
12
|
+
* @returns `true` if `value` is a string, otherwise `false`.
|
|
13
|
+
*/
|
|
14
|
+
export function isString(value) {
|
|
15
|
+
return typeof value === 'string';
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=string.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"string.js","sourceRoot":"","sources":["../../src/predicates/string.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a value is `undefined`. `null` is not considered `undefined`; use {@link isNil} to
|
|
3
|
+
* check for both.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* isUndefined(undefined); // true
|
|
8
|
+
* isUndefined(null); // false
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* @param value - The value to check.
|
|
12
|
+
* @returns `true` if `value` is `undefined`, otherwise `false`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isUndefined<T>(value: T | undefined): value is undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a value is not `undefined`, narrowing `undefined` out of its type. `null` passes
|
|
17
|
+
* this check; use {@link isNotNil} to exclude both.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const lookups = ids.map((id) => cache.get(id)); // (User | undefined)[]
|
|
22
|
+
* const cached = lookups.filter(isNotUndefined); // User[]
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param value - The value to check.
|
|
26
|
+
* @returns `true` if `value` is not `undefined`, otherwise `false`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isNotUndefined<T>(value: T | undefined): value is T;
|
|
29
|
+
//# sourceMappingURL=undefined.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"undefined.d.ts","sourceRoot":"","sources":["../../src/predicates/undefined.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,SAAS,GAAG,KAAK,IAAI,SAAS,CAEvE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,SAAS,GAAG,KAAK,IAAI,CAAC,CAElE"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a value is `undefined`. `null` is not considered `undefined`; use {@link isNil} to
|
|
3
|
+
* check for both.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* isUndefined(undefined); // true
|
|
8
|
+
* isUndefined(null); // false
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* @param value - The value to check.
|
|
12
|
+
* @returns `true` if `value` is `undefined`, otherwise `false`.
|
|
13
|
+
*/
|
|
14
|
+
export function isUndefined(value) {
|
|
15
|
+
return value === undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a value is not `undefined`, narrowing `undefined` out of its type. `null` passes
|
|
19
|
+
* this check; use {@link isNotNil} to exclude both.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const lookups = ids.map((id) => cache.get(id)); // (User | undefined)[]
|
|
24
|
+
* const cached = lookups.filter(isNotUndefined); // User[]
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @param value - The value to check.
|
|
28
|
+
* @returns `true` if `value` is not `undefined`, otherwise `false`.
|
|
29
|
+
*/
|
|
30
|
+
export function isNotUndefined(value) {
|
|
31
|
+
return value !== undefined;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=undefined.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"undefined.js","sourceRoot":"","sources":["../../src/predicates/undefined.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAI,KAAoB;IACjD,OAAO,KAAK,KAAK,SAAS,CAAC;AAC7B,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAI,KAAoB;IACpD,OAAO,KAAK,KAAK,SAAS,CAAC;AAC7B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cult-frog/primitives",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Small, dependency-free TypeScript type guards and callback helpers",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Quin Partridge",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"primitives",
|
|
9
|
+
"utilities",
|
|
10
|
+
"typescript",
|
|
11
|
+
"type-guards",
|
|
12
|
+
"predicates"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/CapnQwan/cult-frog-primitives-js.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/CapnQwan/cult-frog-primitives-js/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/CapnQwan/cult-frog-primitives-js#readme",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20.19"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"lint-staged": {
|
|
44
|
+
"*.{ts,tsx,js}": [
|
|
45
|
+
"biome check --write --error-on-warnings --no-errors-on-unmatched"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@biomejs/biome": "^2.5.13",
|
|
50
|
+
"@cult-frog/tooling": "^0.1.2",
|
|
51
|
+
"@types/node": "^22.20.2",
|
|
52
|
+
"husky": "^9.1.7",
|
|
53
|
+
"lint-staged": "^17.5.1",
|
|
54
|
+
"rimraf": "^6.1.3",
|
|
55
|
+
"typescript": "^7.0.2",
|
|
56
|
+
"vitest": "^5.0.0"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "rimraf dist && tsc -p tsconfig.build.json",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"test:watch": "vitest watch",
|
|
63
|
+
"test:coverage": "vitest run --coverage",
|
|
64
|
+
"check": "biome check . --error-on-warnings",
|
|
65
|
+
"check:ci": "biome ci . --error-on-warnings",
|
|
66
|
+
"fix": "biome check . --write"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { constant } from './constant.js';
|
|
4
|
+
|
|
5
|
+
describe('constant', () => {
|
|
6
|
+
it('returns a function that returns the given value', () => {
|
|
7
|
+
expect(constant('hello')()).toBe('hello');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns the same value on every call', () => {
|
|
11
|
+
const getValue = constant(42);
|
|
12
|
+
|
|
13
|
+
expect(getValue()).toBe(42);
|
|
14
|
+
expect(getValue()).toBe(42);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns the same object reference without copying', () => {
|
|
18
|
+
const value = { id: 1 };
|
|
19
|
+
|
|
20
|
+
expect(constant(value)()).toBe(value);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each([null, undefined, 0, '', false])('returns falsy value %s as-is', (value) => {
|
|
24
|
+
expect(constant(value)()).toBe(value);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('preserves the value type', () => {
|
|
28
|
+
expectTypeOf(constant('hello')).toEqualTypeOf<() => string>();
|
|
29
|
+
expectTypeOf(constant({ id: 1 })).returns.toEqualTypeOf<{ id: number }>();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a function that always returns the given value.
|
|
3
|
+
*
|
|
4
|
+
* Useful when an API expects a callback or factory but you already have the value, e.g. a
|
|
5
|
+
* default provider or a stubbed dependency in tests.
|
|
6
|
+
*
|
|
7
|
+
* The same value is returned on every call; objects are not copied.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const getDefaultName = constant('anonymous');
|
|
12
|
+
* getDefaultName(); // 'anonymous'
|
|
13
|
+
*
|
|
14
|
+
* const names = users.map((user) => user.name ?? getDefaultName());
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @param value - The value the returned function should produce.
|
|
18
|
+
* @returns A function that returns `value` every time it is called.
|
|
19
|
+
*/
|
|
20
|
+
export function constant<T>(value: T): () => T {
|
|
21
|
+
return () => value;
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { identity } from './identity.js';
|
|
4
|
+
|
|
5
|
+
describe('identity', () => {
|
|
6
|
+
it.each([1, 'text', true, null, undefined, 0, ''])('returns %s unchanged', (value) => {
|
|
7
|
+
expect(identity(value)).toBe(value);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns the same object reference', () => {
|
|
11
|
+
const value = { id: 1 };
|
|
12
|
+
|
|
13
|
+
expect(identity(value)).toBe(value);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('works as a default transform', () => {
|
|
17
|
+
expect([1, 2, 3].map(identity)).toEqual([1, 2, 3]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('preserves the value type', () => {
|
|
21
|
+
expectTypeOf(identity('text')).toEqualTypeOf<string>();
|
|
22
|
+
expectTypeOf(identity({ id: 1 })).toEqualTypeOf<{ id: number }>();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the value it is given, unchanged.
|
|
3
|
+
*
|
|
4
|
+
* Useful as a default transform or a pass-through callback, so callers don't need to branch on
|
|
5
|
+
* whether a transform was provided.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* function formatAll<T>(items: T[], format: (item: T) => T = identity): T[] {
|
|
10
|
+
* return items.map(format);
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* formatAll([1, 2, 3]); // [1, 2, 3]
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @param value - The value to return.
|
|
17
|
+
* @returns `value`, unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function identity<T>(value: T): T {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { noop } from './noop.js';
|
|
4
|
+
|
|
5
|
+
describe('noop', () => {
|
|
6
|
+
it('returns undefined', () => {
|
|
7
|
+
expect(noop()).toBeUndefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('can be used where a callback with arguments is expected', () => {
|
|
11
|
+
const onProgress: (percent: number) => void = noop;
|
|
12
|
+
|
|
13
|
+
expect(() => onProgress(50)).not.toThrow();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('has a void return type', () => {
|
|
17
|
+
expectTypeOf(noop).toEqualTypeOf<() => void>();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function that does nothing and returns `undefined`.
|
|
3
|
+
*
|
|
4
|
+
* Useful as a default for optional callbacks, so callers can invoke them without checking
|
|
5
|
+
* whether one was provided. Using a shared `noop` also makes intent clearer than an inline
|
|
6
|
+
* `() => {}`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* function load(onProgress: (percent: number) => void = noop) {
|
|
11
|
+
* onProgress(50);
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function noop(): void {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { once } from './once.js';
|
|
4
|
+
|
|
5
|
+
describe('once', () => {
|
|
6
|
+
it('calls the wrapped function only once', () => {
|
|
7
|
+
const fn = vi.fn(() => 'result');
|
|
8
|
+
const wrapped = once(fn);
|
|
9
|
+
|
|
10
|
+
wrapped();
|
|
11
|
+
wrapped();
|
|
12
|
+
wrapped();
|
|
13
|
+
|
|
14
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns the first result on every call', () => {
|
|
18
|
+
let count = 0;
|
|
19
|
+
const wrapped = once(() => ++count);
|
|
20
|
+
|
|
21
|
+
expect(wrapped()).toBe(1);
|
|
22
|
+
expect(wrapped()).toBe(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('passes arguments from the first call and ignores later ones', () => {
|
|
26
|
+
const fn = vi.fn((a: number, b: number) => a + b);
|
|
27
|
+
const wrapped = once(fn);
|
|
28
|
+
|
|
29
|
+
expect(wrapped(1, 2)).toBe(3);
|
|
30
|
+
expect(wrapped(10, 20)).toBe(3);
|
|
31
|
+
expect(fn).toHaveBeenCalledWith(1, 2);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('caches undefined results without calling again', () => {
|
|
35
|
+
const fn = vi.fn(() => undefined);
|
|
36
|
+
const wrapped = once(fn);
|
|
37
|
+
|
|
38
|
+
wrapped();
|
|
39
|
+
wrapped();
|
|
40
|
+
|
|
41
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('forwards this to the wrapped function', () => {
|
|
45
|
+
const obj = {
|
|
46
|
+
value: 42,
|
|
47
|
+
get: once(function (this: { value: number }) {
|
|
48
|
+
return this.value;
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
expect(obj.get()).toBe(42);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('retries on the next call if the wrapped function throws', () => {
|
|
56
|
+
let attempts = 0;
|
|
57
|
+
const wrapped = once(() => {
|
|
58
|
+
attempts++;
|
|
59
|
+
if (attempts === 1) {
|
|
60
|
+
throw new Error('first attempt fails');
|
|
61
|
+
}
|
|
62
|
+
return 'ok';
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(() => wrapped()).toThrow('first attempt fails');
|
|
66
|
+
expect(wrapped()).toBe('ok');
|
|
67
|
+
expect(wrapped()).toBe('ok');
|
|
68
|
+
expect(attempts).toBe(2);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('runs once in total when used on a shared prototype', () => {
|
|
72
|
+
class Counter {
|
|
73
|
+
static calls = 0;
|
|
74
|
+
|
|
75
|
+
init(): number {
|
|
76
|
+
return ++Counter.calls;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
Counter.prototype.init = once(Counter.prototype.init);
|
|
80
|
+
|
|
81
|
+
new Counter().init();
|
|
82
|
+
new Counter().init();
|
|
83
|
+
|
|
84
|
+
expect(Counter.calls).toBe(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('preserves the parameter and return types', () => {
|
|
88
|
+
const wrapped = once((a: number, b: string) => `${a}${b}`);
|
|
89
|
+
|
|
90
|
+
expectTypeOf(wrapped).parameters.toEqualTypeOf<[number, string]>();
|
|
91
|
+
expectTypeOf(wrapped).returns.toEqualTypeOf<string>();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a function so it runs at most once. Later calls return the result of the first call.
|
|
3
|
+
*
|
|
4
|
+
* Useful for lazy initialisation and for guarding side effects that must only happen once,
|
|
5
|
+
* such as setup, teardown or one-time warnings.
|
|
6
|
+
*
|
|
7
|
+
* - Arguments passed after the first successful call are ignored.
|
|
8
|
+
* - `this` is forwarded to `fn`, so the wrapper can be used as a method.
|
|
9
|
+
* - If `fn` throws, nothing is cached and the next call tries again.
|
|
10
|
+
* - After a successful call the reference to `fn` is released.
|
|
11
|
+
* - When used as a method on a shared prototype, `fn` runs once in total, not once per instance.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const init = once(() => {
|
|
16
|
+
* console.log('initialising');
|
|
17
|
+
* return createClient();
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* init(); // logs 'initialising' and returns the client
|
|
21
|
+
* init(); // returns the same client without logging
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @param fn - The function to wrap.
|
|
25
|
+
* @returns A function with the same signature as `fn` that only invokes it once.
|
|
26
|
+
*/
|
|
27
|
+
export function once<This, Args extends unknown[], R>(
|
|
28
|
+
fn: (this: This, ...args: Args) => R
|
|
29
|
+
): (this: This, ...args: Args) => R {
|
|
30
|
+
let pending: ((this: This, ...args: Args) => R) | undefined = fn;
|
|
31
|
+
let result: R;
|
|
32
|
+
|
|
33
|
+
return function (this: This, ...args: Args): R {
|
|
34
|
+
if (pending) {
|
|
35
|
+
result = pending.apply(this, args);
|
|
36
|
+
pending = undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { isNil, isNotNil } from './nil.js';
|
|
4
|
+
|
|
5
|
+
const nilValues = [null, undefined];
|
|
6
|
+
const nonNilValues = [0, '', false, Number.NaN, [], {}, 'text', 1];
|
|
7
|
+
|
|
8
|
+
describe('isNil', () => {
|
|
9
|
+
it.each(nilValues)('returns true for %s', (value) => {
|
|
10
|
+
expect(isNil(value)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it.each(nonNilValues)('returns false for %s', (value) => {
|
|
14
|
+
expect(isNil(value)).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('narrows to null | undefined', () => {
|
|
18
|
+
const value = 'text' as string | null | undefined;
|
|
19
|
+
|
|
20
|
+
if (isNil(value)) {
|
|
21
|
+
expectTypeOf(value).toEqualTypeOf<null | undefined>();
|
|
22
|
+
} else {
|
|
23
|
+
expectTypeOf(value).toEqualTypeOf<string>();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('isNotNil', () => {
|
|
29
|
+
it.each(nilValues)('returns false for %s', (value) => {
|
|
30
|
+
expect(isNotNil(value)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each(nonNilValues)('returns true for %s', (value) => {
|
|
34
|
+
expect(isNotNil(value)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('removes null and undefined in filter while keeping falsy values', () => {
|
|
38
|
+
const result = [1, null, 0, undefined, 2].filter(isNotNil);
|
|
39
|
+
|
|
40
|
+
expect(result).toEqual([1, 0, 2]);
|
|
41
|
+
expectTypeOf(result).toEqualTypeOf<number[]>();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a value is `null` or `undefined`.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* isNil(null); // true
|
|
7
|
+
* isNil(undefined); // true
|
|
8
|
+
* isNil(0); // false
|
|
9
|
+
* isNil(''); // false
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* @param value - The value to check.
|
|
13
|
+
* @returns `true` if `value` is `null` or `undefined`, otherwise `false`.
|
|
14
|
+
*/
|
|
15
|
+
export function isNil<T>(value: T | null | undefined): value is null | undefined {
|
|
16
|
+
return value == null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if a value is neither `null` nor `undefined`, narrowing it to its non-nullable type.
|
|
21
|
+
*
|
|
22
|
+
* Unlike a truthiness check, falsy values such as `0`, `''` and `false` are kept.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const values = [1, null, 0, undefined, 2];
|
|
27
|
+
* const present = values.filter(isNotNil); // number[]: [1, 0, 2]
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @param value - The value to check.
|
|
31
|
+
* @returns `true` if `value` is not `null` or `undefined`, otherwise `false`.
|
|
32
|
+
*/
|
|
33
|
+
export function isNotNil<T>(value: T | null | undefined): value is T {
|
|
34
|
+
return value != null;
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { isNotNull, isNull } from './null.js';
|
|
4
|
+
|
|
5
|
+
const nonNullValues = [undefined, 0, '', false, {}, 'text'];
|
|
6
|
+
|
|
7
|
+
describe('isNull', () => {
|
|
8
|
+
it('returns true for null', () => {
|
|
9
|
+
expect(isNull(null)).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it.each(nonNullValues)('returns false for %s', (value) => {
|
|
13
|
+
expect(isNull(value)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('narrows to null', () => {
|
|
17
|
+
const value = 'text' as string | null;
|
|
18
|
+
|
|
19
|
+
if (isNull(value)) {
|
|
20
|
+
expectTypeOf(value).toEqualTypeOf<null>();
|
|
21
|
+
} else {
|
|
22
|
+
expectTypeOf(value).toEqualTypeOf<string>();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('isNotNull', () => {
|
|
28
|
+
it('returns false for null', () => {
|
|
29
|
+
expect(isNotNull(null)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it.each(nonNullValues)('returns true for %s', (value) => {
|
|
33
|
+
expect(isNotNull(value)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('removes null in filter but keeps undefined', () => {
|
|
37
|
+
const result = [1, null, undefined].filter(isNotNull);
|
|
38
|
+
|
|
39
|
+
expect(result).toEqual([1, undefined]);
|
|
40
|
+
expectTypeOf(result).toEqualTypeOf<(number | undefined)[]>();
|
|
41
|
+
});
|
|
42
|
+
});
|