From ffa77c42363cf75e804a5c2d0e593c9c0c6101b4 Mon Sep 17 00:00:00 2001 From: jossmac Date: Fri, 27 May 2022 07:53:30 +1000 Subject: [PATCH 1/4] move error stuff to utils dir --- docs/components/Shell/SideNav.js | 1 - docs/pages/docs/errors.md | 62 ---------------- docs/pages/docs/utils.md | 65 ++++++++++++++++- src/assertions.test.ts | 2 +- src/index.ts | 4 +- src/runtime.test.ts | 79 --------------------- src/runtime.ts | 19 ----- src/testing.ts | 2 +- src/types.ts | 2 + src/utils.test.ts | 21 ------ src/{errors.test.ts => utils/error.test.ts} | 4 +- src/{errors.ts => utils/error.ts} | 2 +- src/utils/object.test.ts | 19 +++++ src/{utils.ts => utils/object.ts} | 0 14 files changed, 91 insertions(+), 191 deletions(-) delete mode 100644 docs/pages/docs/errors.md delete mode 100644 src/runtime.test.ts delete mode 100644 src/runtime.ts delete mode 100644 src/utils.test.ts rename src/{errors.test.ts => utils/error.test.ts} (92%) rename src/{errors.ts => utils/error.ts} (95%) create mode 100644 src/utils/object.test.ts rename src/{utils.ts => utils/object.ts} (100%) diff --git a/docs/components/Shell/SideNav.js b/docs/components/Shell/SideNav.js index 5d89cfe..d18169e 100644 --- a/docs/components/Shell/SideNav.js +++ b/docs/components/Shell/SideNav.js @@ -15,7 +15,6 @@ const items = [ links: [ { href: '/docs/assertions', children: 'Assertions' }, { href: '/docs/checks', children: 'Checks' }, - { href: '/docs/errors', children: 'Errors' }, { href: '/docs/guards', children: 'Guards' }, { href: '/docs/opaques', children: 'Opaques' }, { href: '/docs/utils', children: 'Utils' }, diff --git a/docs/pages/docs/errors.md b/docs/pages/docs/errors.md deleted file mode 100644 index 5dd82c2..0000000 --- a/docs/pages/docs/errors.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Errors -description: Utilities for managing errors ---- - -# {% $markdoc.frontmatter.title %} - -Utilities for managing errors in [try...catch](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) statements. - -## Functions - -### getErrorMessage - -Simplifies `error` handling in `try...catch` statements. - -```ts -function getErrorMessage(error: unknown, fallbackMessage? string): string -``` - -JavaScript is weird, you can `throw` anything—seriously, [anything of any type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw). - -```ts -try { - someFunctionThatMightThrow(); -} catch (error) { - Monitor.reportError(error.message); - // ~~~~~ - // Object is of type 'unknown'. -} -``` - -#### Type casting - -Since it's possible for library authors to throw something unexpected, we have to take precautions. Using `getErrorMessage` takes care of type casting for you, and makes error handling safe and simple. - -```ts -Monitor.reportError(getErrorMessage(error)); -// 🎉 No more TypeScript issues! -``` - -Handles cases where the value isn't an actual `Error` object. - -```ts -getErrorMessage({ message: 'Object text', other: () => 'Properties' }); -// → 'Object text' -``` - -Supports a fallback message for "falsy" values. - -```ts -getErrorMessage(undefined); -// → 'Unknown error' -getErrorMessage(undefined, 'Custom message text'); -// → 'Custom message text' -``` - -Fails gracefully by stringifying unexpected values. - -```ts -getErrorMessage({ msg: 'Something went wrong' }); -// → '{ "msg": "Something went wrong" }' -``` diff --git a/docs/pages/docs/utils.md b/docs/pages/docs/utils.md index 026bce3..8fdb2a3 100644 --- a/docs/pages/docs/utils.md +++ b/docs/pages/docs/utils.md @@ -3,13 +3,69 @@ title: Utils description: Utility functions for overiding TypeScript's default behaviour --- -# {% $markdoc.frontmatter.title %} +# Utils Utility functions for overiding TypeScript's default behaviour +## Errors + +Utilities for managing [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) objects. + +### getErrorMessage + +Simplifies error handling in `try...catch` statements. + +```ts +function getErrorMessage(error: unknown, fallbackMessage? string): string +``` + +JavaScript is weird, you can `throw` anything—seriously, [anything of any type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw). + +```ts +try { + someFunctionThatMightThrow(); +} catch (error) { + Monitor.reportError(error.message); + // ~~~~~ + // Object is of type 'unknown'. +} +``` + +#### Type casting + +Since it's possible for library authors to throw something unexpected, we have to take precautions. Using `getErrorMessage` takes care of type casting for you, and makes error handling safe and simple. + +```ts +Monitor.reportError(getErrorMessage(error)); +// 🎉 No more TypeScript issues! +``` + +Handles cases where the value isn't an actual `Error` object. + +```ts +getErrorMessage({ message: 'Object text', other: () => 'Properties' }); +// → 'Object text' +``` + +Supports a fallback message for "falsy" values. + +```ts +getErrorMessage(undefined); +// → 'Unknown error' +getErrorMessage(undefined, 'Custom message text'); +// → 'Custom message text' +``` + +Fails gracefully by stringifying unexpected values. + +```ts +getErrorMessage({ msg: 'Something went wrong' }); +// → '{ "msg": "Something went wrong" }' +``` + ## Objects -Utility functions for objects. +Utility functions for [objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object). ### typedEntries @@ -59,4 +115,9 @@ const thing = Object.keys(obj).map(key => { // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ foo: number; bar: number; }'. // No index signature with a parameter of type 'string' was found on type '{ foo: number; bar: number; }'. }); + +const thing2 = typedKeys(obj).map(key => { + return obj[key]; + // 🎉 No more TypeScript issues! +}); ``` diff --git a/src/assertions.test.ts b/src/assertions.test.ts index 2e5a16c..9acd3d1 100644 --- a/src/assertions.test.ts +++ b/src/assertions.test.ts @@ -1,5 +1,5 @@ import { assert, assertNever } from './assertions'; -import { getErrorMessage } from './errors'; +import { getErrorMessage } from './utils/error'; describe('assertions', () => { describe('assert', () => { diff --git a/src/index.ts b/src/index.ts index ac14c6b..340ce31 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,6 @@ export { isPositive, } from './checks'; -export { getErrorMessage } from './errors'; export { isBoolean, @@ -31,7 +30,8 @@ export { toOpaque, toTransparent } from './opaques'; export { checkAll, checkAllWith, negate } from './runtime'; -export { typedEntries, typedKeys } from './utils'; +export { getErrorMessage } from './utils/error'; +export { typedEntries, typedKeys } from './utils/object'; // Types // ------------------------------ diff --git a/src/runtime.test.ts b/src/runtime.test.ts deleted file mode 100644 index 6ad7443..0000000 --- a/src/runtime.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { checkAll, checkAllWith, negate } from './runtime'; - -describe('runtime', () => { - const isEven = jest.fn(x => x % 2 === 0); - const isNumberish = jest.fn(x => typeof x === 'number'); - const lessThanTen = jest.fn(x => x < 10); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('negate', () => { - it('should return a negated predicate', () => { - const isOdd = negate(isEven); - - expect(isOdd(4)).toEqual(false); - expect(isEven).toHaveBeenCalledTimes(1); - expect(isEven).toHaveBeenCalledWith(4); - }); - }); - - describe('checkAll', () => { - it('should create a new function', () => { - expect(checkAll(isEven)).toBeDefined(); - }); - it('should call provided fns, and return correct result', () => { - const checker = jest.fn(checkAll(isNumberish, lessThanTen, isEven)); - const bool = checker(4); - - expect(isNumberish).toHaveBeenCalledTimes(1); - expect(lessThanTen).toHaveBeenCalledTimes(1); - expect(isEven).toHaveBeenCalledTimes(1); - - expect(checker).toHaveBeenCalled(); - expect(checker).toHaveBeenCalledWith(4); - expect(bool).toEqual(true); - }); - it('should call provided fns in sequence, and early exit when appropriate', () => { - const checker = jest.fn(checkAll(isNumberish, lessThanTen, isEven)); - const bool = checker(25); - - expect(isNumberish).toHaveBeenCalledTimes(1); - expect(lessThanTen).toHaveBeenCalledTimes(1); - expect(isEven).toHaveBeenCalledTimes(0); - - expect(checker).toHaveBeenCalledWith(25); - expect(bool).toEqual(false); - }); - it('should support built-ins', () => { - const checker = jest.fn(checkAll(Array.isArray)); - const bool = checker([]); - - expect(checker).toHaveBeenCalledWith([]); - expect(bool).toEqual(true); - }); - }); - - describe('checkAllWith', () => { - it('should return the correct result', () => { - expect(checkAllWith(5, isEven)).toBe(false); - }); - it('should call provided fns, and return correct result', () => { - const bool = checkAllWith(4, isNumberish, lessThanTen, isEven); - - expect(isNumberish).toHaveBeenCalledTimes(1); - expect(lessThanTen).toHaveBeenCalledTimes(1); - expect(isEven).toHaveBeenCalledTimes(1); - expect(bool).toEqual(true); - }); - it('should call provided fns in sequence, and early exit when appropriate', () => { - const bool = checkAllWith(25, isNumberish, lessThanTen, isEven); - - expect(isNumberish).toHaveBeenCalledTimes(1); - expect(lessThanTen).toHaveBeenCalledTimes(1); - expect(isEven).toHaveBeenCalledTimes(0); - expect(bool).toEqual(false); - }); - }); -}); diff --git a/src/runtime.ts b/src/runtime.ts deleted file mode 100644 index 04f23ca..0000000 --- a/src/runtime.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { UnaryPredicate } from './types'; - -/** - * Returns a new function for checking *all* cases against a value, a bit - * like `pipe` for predicates. - */ -export function checkAll(...predicates: UnaryPredicate[]) { - return (value: T) => predicates.every(p => p(value)); -} - -/** Apply *all* checks against a value. */ -export function checkAllWith(value: T, ...predicates: UnaryPredicate[]) { - return checkAll(...predicates)(value); -} - -/** Returns a new negated version of the stated predicate function. */ -export function negate(predicate: UnaryPredicate) { - return (value: T) => !predicate(value); -} diff --git a/src/testing.ts b/src/testing.ts index a4dd535..62e626d 100644 --- a/src/testing.ts +++ b/src/testing.ts @@ -1,4 +1,4 @@ -import { typedKeys } from './utils'; +import { typedKeys } from './utils/object'; const obj = { a: 1, b: 2, c: 'three' }; const arr = Object.values(obj); diff --git a/src/types.ts b/src/types.ts index 6edc38f..0a288c1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,8 @@ export type ObjectEntry = { [K in keyof T]: [K, T[K]] }[keyof T]; export type Nullish = null | undefined; +export type ErrorLike = { message: string }; + // Opaque types // ------------------------------ diff --git a/src/utils.test.ts b/src/utils.test.ts deleted file mode 100644 index 99a7c42..0000000 --- a/src/utils.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { typedEntries, typedKeys } from './utils'; - -describe('utils', () => { - describe('object', () => { - describe('typedEntries', () => { - it('should return the correct entries', () => { - const result = typedEntries({ foo: 1, bar: 2 }); - expect(result).toEqual([ - ['foo', 1], - ['bar', 2], - ]); - }); - }); - describe('typedKeys', () => { - it('should return the correct keys', () => { - const result = typedKeys({ foo: 1, bar: 2 }); - expect(result).toEqual(['foo', 'bar']); - }); - }); - }); -}); diff --git a/src/errors.test.ts b/src/utils/error.test.ts similarity index 92% rename from src/errors.test.ts rename to src/utils/error.test.ts index dbcae31..04d671c 100644 --- a/src/errors.test.ts +++ b/src/utils/error.test.ts @@ -1,6 +1,6 @@ -import { getErrorMessage } from './errors'; +import { getErrorMessage } from './error'; -describe('errors', () => { +describe('utils/errors', () => { describe('getErrorMessage', () => { it('should validate real errors', () => { expect(getErrorMessage(new Error('Error text'))).toBe('Error text'); diff --git a/src/errors.ts b/src/utils/error.ts similarity index 95% rename from src/errors.ts rename to src/utils/error.ts index 49de33b..872ab3b 100644 --- a/src/errors.ts +++ b/src/utils/error.ts @@ -1,4 +1,4 @@ -type ErrorLike = { message: string }; +import { ErrorLike } from '../types'; /** * Simplifies `error` handling in `try...catch` statements. diff --git a/src/utils/object.test.ts b/src/utils/object.test.ts new file mode 100644 index 0000000..ce8ce73 --- /dev/null +++ b/src/utils/object.test.ts @@ -0,0 +1,19 @@ +import { typedEntries, typedKeys } from './object'; + +describe('utils/object', () => { + describe('typedEntries', () => { + it('should return the correct entries', () => { + const result = typedEntries({ foo: 1, bar: 2 }); + expect(result).toEqual([ + ['foo', 1], + ['bar', 2], + ]); + }); + }); + describe('typedKeys', () => { + it('should return the correct keys', () => { + const result = typedKeys({ foo: 1, bar: 2 }); + expect(result).toEqual(['foo', 'bar']); + }); + }); +}); diff --git a/src/utils.ts b/src/utils/object.ts similarity index 100% rename from src/utils.ts rename to src/utils/object.ts From e456f2466516458e19ed9caddaa0024f864c3087 Mon Sep 17 00:00:00 2001 From: jossmac Date: Fri, 27 May 2022 07:54:03 +1000 Subject: [PATCH 2/4] combine checks stuff under its own dir --- src/checks.test.ts | 97 ----------------------------- src/checks/number.test.ts | 95 ++++++++++++++++++++++++++++ src/{checks.ts => checks/number.ts} | 10 +-- src/checks/utils.test.ts | 79 +++++++++++++++++++++++ src/checks/utils.ts | 19 ++++++ src/index.ts | 6 +- 6 files changed, 197 insertions(+), 109 deletions(-) delete mode 100644 src/checks.test.ts create mode 100644 src/checks/number.test.ts rename src/{checks.ts => checks/number.ts} (78%) create mode 100644 src/checks/utils.test.ts create mode 100644 src/checks/utils.ts diff --git a/src/checks.test.ts b/src/checks.test.ts deleted file mode 100644 index 3af232e..0000000 --- a/src/checks.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - isEven, - isFinite, - isFloat, - isInfinite, - isInteger, - isNegative, - isNegativeZero, - isNonNegative, - isNonPositive, - isOdd, - isPositive, -} from './checks'; - -describe('checks', () => { - describe('number', () => { - it('`isFinite` should correctly evaluate values', () => { - expect(isFinite(1)).toBe(true); - expect(isFinite(123e4 / 12.3)).toBe(true); - expect(isFinite(Number.MAX_SAFE_INTEGER)).toBe(true); - expect(isFinite(Number.MIN_SAFE_INTEGER)).toBe(true); - expect(isFinite(Number.POSITIVE_INFINITY)).toBe(false); - expect(isFinite(Number.NEGATIVE_INFINITY)).toBe(false); - }); - it('`isInfinite` should correctly evaluate values', () => { - expect(isInfinite(Number.POSITIVE_INFINITY)).toBe(true); - expect(isInfinite(Number.NEGATIVE_INFINITY)).toBe(true); - expect(isInfinite(1)).toBe(false); - expect(isInfinite(123e4 / 12.3)).toBe(false); - expect(isInfinite(Number.MAX_SAFE_INTEGER)).toBe(false); - expect(isInfinite(Number.MIN_SAFE_INTEGER)).toBe(false); - }); - - it('`isInteger` should correctly evaluate values', () => { - expect(isInteger(1)).toBe(true); - expect(isInteger(-1)).toBe(true); - expect(isInteger(1.23)).toBe(false); - expect(isInteger(-1.23)).toBe(false); - }); - it('`isFloat` should correctly evaluate values', () => { - expect(isFloat(1.23)).toBe(true); - expect(isFloat(-1.23)).toBe(true); - expect(isFloat(1)).toBe(false); - expect(isFloat(-1)).toBe(false); - }); - - it('`isEven` should correctly evaluate values', () => { - expect(isEven(2)).toBe(true); - expect(isEven(-22)).toBe(true); - expect(isEven(1)).toBe(false); - expect(isEven(2.2)).toBe(false); - }); - it('`isOdd` should correctly evaluate values', () => { - expect(isOdd(1)).toBe(true); - expect(isOdd(-11)).toBe(true); - expect(isOdd(2)).toBe(false); - expect(isOdd(1.1)).toBe(false); - }); - - it('`isNegativeZero` should correctly evaluate values', () => { - expect(isNegativeZero(-0)).toBe(true); - expect(isNegativeZero(1)).toBe(false); - expect(isNegativeZero(0)).toBe(false); - }); - - it('`isNegative` should correctly evaluate values', () => { - expect(isNegative(-1)).toBe(true); - expect(isNegative(-1.23)).toBe(true); - expect(isNegative(1)).toBe(false); - expect(isNegative(0)).toBe(false); - expect(isNegative(-0)).toBe(false); - }); - it('`isPositive` should correctly evaluate values', () => { - expect(isPositive(1)).toBe(true); - expect(isPositive(1.23)).toBe(true); - expect(isPositive(-1)).toBe(false); - expect(isPositive(0)).toBe(false); - expect(isPositive(-0)).toBe(false); - }); - - it('`isNonNegative` should correctly evaluate values', () => { - expect(isNonNegative(0)).toBe(true); - expect(isNonNegative(-0)).toBe(true); - expect(isNonNegative(1)).toBe(true); - expect(isNonNegative(1.23)).toBe(true); - expect(isNonNegative(-1)).toBe(false); - expect(isNonNegative(-1.23)).toBe(false); - }); - it('`isNonPositive` should correctly evaluate values', () => { - expect(isNonPositive(0)).toBe(true); - expect(isNonPositive(-0)).toBe(true); - expect(isNonPositive(-1)).toBe(true); - expect(isNonPositive(1)).toBe(false); - expect(isNonPositive(1.23)).toBe(false); - }); - }); -}); diff --git a/src/checks/number.test.ts b/src/checks/number.test.ts new file mode 100644 index 0000000..3e28f7f --- /dev/null +++ b/src/checks/number.test.ts @@ -0,0 +1,95 @@ +import { + isEven, + isFinite, + isFloat, + isInfinite, + isInteger, + isNegative, + isNegativeZero, + isNonNegative, + isNonPositive, + isOdd, + isPositive, +} from './number'; + +describe('checks/number', () => { + it('`isFinite` should correctly evaluate values', () => { + expect(isFinite(1)).toBe(true); + expect(isFinite(123e4 / 12.3)).toBe(true); + expect(isFinite(Number.MAX_SAFE_INTEGER)).toBe(true); + expect(isFinite(Number.MIN_SAFE_INTEGER)).toBe(true); + expect(isFinite(Number.POSITIVE_INFINITY)).toBe(false); + expect(isFinite(Number.NEGATIVE_INFINITY)).toBe(false); + }); + it('`isInfinite` should correctly evaluate values', () => { + expect(isInfinite(Number.POSITIVE_INFINITY)).toBe(true); + expect(isInfinite(Number.NEGATIVE_INFINITY)).toBe(true); + expect(isInfinite(1)).toBe(false); + expect(isInfinite(123e4 / 12.3)).toBe(false); + expect(isInfinite(Number.MAX_SAFE_INTEGER)).toBe(false); + expect(isInfinite(Number.MIN_SAFE_INTEGER)).toBe(false); + }); + + it('`isInteger` should correctly evaluate values', () => { + expect(isInteger(1)).toBe(true); + expect(isInteger(-1)).toBe(true); + expect(isInteger(1.23)).toBe(false); + expect(isInteger(-1.23)).toBe(false); + }); + it('`isFloat` should correctly evaluate values', () => { + expect(isFloat(1.23)).toBe(true); + expect(isFloat(-1.23)).toBe(true); + expect(isFloat(1)).toBe(false); + expect(isFloat(-1)).toBe(false); + }); + + it('`isEven` should correctly evaluate values', () => { + expect(isEven(2)).toBe(true); + expect(isEven(-22)).toBe(true); + expect(isEven(1)).toBe(false); + expect(isEven(2.2)).toBe(false); + }); + it('`isOdd` should correctly evaluate values', () => { + expect(isOdd(1)).toBe(true); + expect(isOdd(-11)).toBe(true); + expect(isOdd(2)).toBe(false); + expect(isOdd(1.1)).toBe(false); + }); + + it('`isNegativeZero` should correctly evaluate values', () => { + expect(isNegativeZero(-0)).toBe(true); + expect(isNegativeZero(1)).toBe(false); + expect(isNegativeZero(0)).toBe(false); + }); + + it('`isNegative` should correctly evaluate values', () => { + expect(isNegative(-1)).toBe(true); + expect(isNegative(-1.23)).toBe(true); + expect(isNegative(1)).toBe(false); + expect(isNegative(0)).toBe(false); + expect(isNegative(-0)).toBe(false); + }); + it('`isPositive` should correctly evaluate values', () => { + expect(isPositive(1)).toBe(true); + expect(isPositive(1.23)).toBe(true); + expect(isPositive(-1)).toBe(false); + expect(isPositive(0)).toBe(false); + expect(isPositive(-0)).toBe(false); + }); + + it('`isNonNegative` should correctly evaluate values', () => { + expect(isNonNegative(0)).toBe(true); + expect(isNonNegative(-0)).toBe(true); + expect(isNonNegative(1)).toBe(true); + expect(isNonNegative(1.23)).toBe(true); + expect(isNonNegative(-1)).toBe(false); + expect(isNonNegative(-1.23)).toBe(false); + }); + it('`isNonPositive` should correctly evaluate values', () => { + expect(isNonPositive(0)).toBe(true); + expect(isNonPositive(-0)).toBe(true); + expect(isNonPositive(-1)).toBe(true); + expect(isNonPositive(1)).toBe(false); + expect(isNonPositive(1.23)).toBe(false); + }); +}); diff --git a/src/checks.ts b/src/checks/number.ts similarity index 78% rename from src/checks.ts rename to src/checks/number.ts index e77b66b..def5100 100644 --- a/src/checks.ts +++ b/src/checks/number.ts @@ -1,12 +1,6 @@ -import { negate } from './runtime'; -import { UnaryPredicate } from './types'; +import { UnaryPredicate } from '../types'; -// ============================================================== -// NOTE: Checks are predicates that cannot be expressed as guards -// ============================================================== - -// Number -// ------------------------------ +import { negate } from './utils'; /** Checks whether a number is a finite */ export const isFinite: UnaryPredicate = Number.isFinite; diff --git a/src/checks/utils.test.ts b/src/checks/utils.test.ts new file mode 100644 index 0000000..76552a8 --- /dev/null +++ b/src/checks/utils.test.ts @@ -0,0 +1,79 @@ +import { checkAll, checkAllWith, negate } from './utils'; + +describe('checks/utils', () => { + const isEven = jest.fn(x => x % 2 === 0); + const isNumberish = jest.fn(x => typeof x === 'number'); + const lessThanTen = jest.fn(x => x < 10); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('negate', () => { + it('should return a negated predicate', () => { + const isOdd = negate(isEven); + + expect(isOdd(4)).toEqual(false); + expect(isEven).toHaveBeenCalledTimes(1); + expect(isEven).toHaveBeenCalledWith(4); + }); + }); + + describe('checkAll', () => { + it('should create a new function', () => { + expect(checkAll(isEven)).toBeDefined(); + }); + it('should call provided fns, and return correct result', () => { + const checker = jest.fn(checkAll(isNumberish, lessThanTen, isEven)); + const bool = checker(4); + + expect(isNumberish).toHaveBeenCalledTimes(1); + expect(lessThanTen).toHaveBeenCalledTimes(1); + expect(isEven).toHaveBeenCalledTimes(1); + + expect(checker).toHaveBeenCalled(); + expect(checker).toHaveBeenCalledWith(4); + expect(bool).toEqual(true); + }); + it('should call provided fns in sequence, and early exit when appropriate', () => { + const checker = jest.fn(checkAll(isNumberish, lessThanTen, isEven)); + const bool = checker(25); + + expect(isNumberish).toHaveBeenCalledTimes(1); + expect(lessThanTen).toHaveBeenCalledTimes(1); + expect(isEven).toHaveBeenCalledTimes(0); + + expect(checker).toHaveBeenCalledWith(25); + expect(bool).toEqual(false); + }); + it('should support built-ins', () => { + const checker = jest.fn(checkAll(Array.isArray)); + const bool = checker([]); + + expect(checker).toHaveBeenCalledWith([]); + expect(bool).toEqual(true); + }); + }); + + describe('checkAllWith', () => { + it('should return the correct result', () => { + expect(checkAllWith(5, isEven)).toBe(false); + }); + it('should call provided fns, and return correct result', () => { + const bool = checkAllWith(4, isNumberish, lessThanTen, isEven); + + expect(isNumberish).toHaveBeenCalledTimes(1); + expect(lessThanTen).toHaveBeenCalledTimes(1); + expect(isEven).toHaveBeenCalledTimes(1); + expect(bool).toEqual(true); + }); + it('should call provided fns in sequence, and early exit when appropriate', () => { + const bool = checkAllWith(25, isNumberish, lessThanTen, isEven); + + expect(isNumberish).toHaveBeenCalledTimes(1); + expect(lessThanTen).toHaveBeenCalledTimes(1); + expect(isEven).toHaveBeenCalledTimes(0); + expect(bool).toEqual(false); + }); + }); +}); diff --git a/src/checks/utils.ts b/src/checks/utils.ts new file mode 100644 index 0000000..7cc3949 --- /dev/null +++ b/src/checks/utils.ts @@ -0,0 +1,19 @@ +import { UnaryPredicate } from '../types'; + +/** + * Returns a new function for checking *all* cases against a value, a bit + * like `pipe` for predicates. + */ +export function checkAll(...predicates: UnaryPredicate[]) { + return (value: T) => predicates.every(p => p(value)); +} + +/** Apply *all* checks against a value. */ +export function checkAllWith(value: T, ...predicates: UnaryPredicate[]) { + return checkAll(...predicates)(value); +} + +/** Returns a new negated version of the stated predicate function. */ +export function negate(predicate: UnaryPredicate) { + return (value: T) => !predicate(value); +} diff --git a/src/index.ts b/src/index.ts index 340ce31..f9506d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,8 +12,8 @@ export { isNonNegative, isNonPositive, isPositive, -} from './checks'; - +} from './checks/number'; +export { checkAll, checkAllWith, negate } from './checks/utils'; export { isBoolean, @@ -28,8 +28,6 @@ export { export { toOpaque, toTransparent } from './opaques'; -export { checkAll, checkAllWith, negate } from './runtime'; - export { getErrorMessage } from './utils/error'; export { typedEntries, typedKeys } from './utils/object'; From 546646ba94818e0762286085876a6e71150e6e6f Mon Sep 17 00:00:00 2001 From: jossmac Date: Fri, 27 May 2022 08:32:06 +1000 Subject: [PATCH 3/4] tweak opaque docs --- docs/pages/docs/opaques.md | 38 ++++++++++++++++++++++++++++---------- docs/pages/docs/utils.md | 2 +- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/docs/pages/docs/opaques.md b/docs/pages/docs/opaques.md index 4cc03f7..3aff9ec 100644 --- a/docs/pages/docs/opaques.md +++ b/docs/pages/docs/opaques.md @@ -35,7 +35,7 @@ type ThingThree = Opaque; // 🚨 Non-unique `Token` parameter ``` -As a side note, you can (and should) use recursive types for your opaque types to make them stronger and hopefully easier to type. +You can, and should, use recursive types for your opaque types to make them stronger and hopefully easier to type. ```ts type Person = { @@ -66,28 +66,46 @@ A generic helper function that takes a primitive value, and returns the value af function toOpaque(value: bigint | number | string | symbol): OpaqueType; ``` -Must be used in combination with the `Opaque` [generic type](#opaque). +Opaque types cannot be assigned to variables with standard type declarations—this is by design, ensuring that opaquely typed values flow through the program without degrading. + +```ts +const value: AccountNumber = 123; +// ~~~~~ +// Type 'number' is not assignable to type 'AccountNumber'. +``` + +Instead use `toOpaque` to create values of opaque types. ```ts -type NumericThing = Opaque; +type AccountNumber = Opaque; const value = 123; // → 'value' is 'number' -const opaqueValue = toOpaque(value); -// → 'opaqueValue' is 'NumericThing' +const opaqueValue = toOpaque(value); +// → 'opaqueValue' is 'AccountNumber' +``` + +Ideally, each opaque type would have a companion function for managing their creation. + +```ts +export type AccountNumber = Opaque; + +export function createAccountNumber(value: number) { + return toOpaque(value); +} ``` -Ensures basic type safety before casting. +Ensures basic type safety before casting to avoid invalid primitive assignment. ```ts -const thingTwo = toOpaque('123'); -// ~~~~~ -// Argument of type 'string' is not assignable to parameter of type 'number'. +const value = toOpaque('123'); +// ~~~~~ +// Argument of type 'string' is not assignable to parameter of type 'number'. ``` ### toTransparent -A generic helper function that takes an opaquely typed value, and returns the value after widening it to the primitive transparent type. +A generic helper function that takes an opaquely typed value, and returns the value after widening it to the transparent primitive type. ```ts function toTransparent(value: OpaqueType): bigint | number | string | symbol; diff --git a/docs/pages/docs/utils.md b/docs/pages/docs/utils.md index 8fdb2a3..6283199 100644 --- a/docs/pages/docs/utils.md +++ b/docs/pages/docs/utils.md @@ -3,7 +3,7 @@ title: Utils description: Utility functions for overiding TypeScript's default behaviour --- -# Utils +# {% $markdoc.frontmatter.title %} Utility functions for overiding TypeScript's default behaviour From d60a301020caad17176f55c018ef33bf0b187c39 Mon Sep 17 00:00:00 2001 From: jossmac Date: Fri, 27 May 2022 08:37:46 +1000 Subject: [PATCH 4/4] lint fix --- src/utils/object.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/object.ts b/src/utils/object.ts index 73dc5fd..22ba595 100644 --- a/src/utils/object.ts +++ b/src/utils/object.ts @@ -1,4 +1,4 @@ -import { ObjectEntry } from './types'; +import { ObjectEntry } from '../types'; /** * An alternative to `Object.entries()` that avoids type widening.