Skip to content

Commit

Permalink
Warning (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
jossmac committed Jul 7, 2022
1 parent 61df49d commit 810b5bf
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/hot-coats-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'emery': minor
---

Add `warning` fn
29 changes: 22 additions & 7 deletions docs/pages/docs/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ An assertion declares that a condition be `true` before executing subsequent cod
- If the condition resolves to `true` the code continues running.
- If the condition resolves to `false` an error will be thrown.

## Functions

TypeScript supports the `asserts` keyword, for use in the return statement of [assertion functions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions).
## Errors

### assert

Expand Down Expand Up @@ -42,8 +40,8 @@ assert(false);
Or you can provide a custom message:

```ts
let falsyValue = -1;
assert(falsyValue >= 0, `Expected a non-negative number, but received: ${falsyValue}`);
let invalidValue = -1;
assert(invalidValue >= 0, `Expected a non-negative number, but received: ${invalidValue}`);
// → TypeError: Expected a non-negative number, but received: -1
```

Expand Down Expand Up @@ -80,6 +78,23 @@ doThing('archived');
// → Error: Unexpected call to assertNever: 'archived'
```

## Debugging
## Logs

### warning

Similar to `assert` but only logs a warning if the condition is not met.

```ts
function warning(condition: boolean, message: string): void;
```

Use `warning` to let consumers know about potentially hazardous circumstances.

{% callout %}
Never logs in production.
{% /callout %}

In **development** both `assert` and `assertNever` will include a [debugger statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger), which will pause execution to aid debugging.
```ts
warning(options.scrollEventThrottle === 0, 'Unthrottled scroll handler may harm performance.');
// → console.warn('Warning: Unthrottled scroll handler may harm performance.');
```
30 changes: 26 additions & 4 deletions src/assertions.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assert, assertNever } from './assertions';
import { assert, assertNever, warning } from './assertions';
import { getErrorMessage } from './utils/error';
import { falsyValues, truthyValues } from './testing';

describe('assertions', () => {
describe('assert', () => {
Expand All @@ -11,9 +12,6 @@ describe('assertions', () => {
});

it('should expect TS error when called with non-boolean conditions', () => {
const falsyValues = [0, -0, '', null, undefined, NaN];
const truthyValues = [1, -1, 'test', {}, [], Number.POSITIVE_INFINITY];

falsyValues.forEach(val => {
// @ts-expect-error should not accept non-boolean conditions
expect(() => assert(val)).toThrow();
Expand Down Expand Up @@ -62,4 +60,28 @@ describe('assertions', () => {
}
});
});

describe('warning', () => {
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
jest.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
// @ts-expect-error: mocked
console.warn.mockRestore();
});

it('should not warn if the condition is true', () => {
warning(true, 'test message');
expect(console.warn).not.toHaveBeenCalled();
});
it('should warn if the condition is false', () => {
const message = 'test message';
warning(false, message);

expect(console.warn).toHaveBeenCalledWith('Warning: ' + message);
// @ts-expect-error: mocked
console.warn.mockClear();
});
});
});
43 changes: 26 additions & 17 deletions src/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,13 @@
* checked must be true for the remainder of the containing scope.
*
* @throws when the condition is `false`
* @returns void
*/
// NOTE: The narrow type of `boolean` instead of something like `unknown` is an
// intentional design decision. The goal is to promote consideration from
// consumers when dealing with potentially ambiguous conditions like `0` or
// `''`, which can introduce "subtle" bugs.
export function assert(
condition: boolean,
message = 'Assert failed',
options = { debug: true },
): asserts condition {
export function assert(condition: boolean, message = 'Assert failed'): asserts condition {
if (!condition) {
developmentDebugger(options.debug);
throw new TypeError(message);
}
}
Expand All @@ -24,19 +18,34 @@ export function assert(
* Asserts that allegedly unreachable code has been executed.
*
* @throws always
* @returns void
*/
export function assertNever(condition: never, options = { debug: true }): never {
developmentDebugger(options.debug);
export function assertNever(condition: never): never {
throw new Error(`Unexpected call to assertNever: '${condition}'`);
}

/** Pause execution in development to aid debugging. */
function developmentDebugger(enabled?: boolean): void {
if (!enabled || process.env.NODE_ENV === 'production') {
return;
}
/**
* Similar to `assert` but only logs a warning if the condition is not met. Only
* logs in development.
*/
export function warning(condition: boolean, message: string) {
if (!(process.env.NODE_ENV === 'production')) {
if (condition) {
return;
}

// eslint-disable-next-line no-debugger
debugger;
// follow message prefix convention
const text = `Warning: ${message}`;

// IE9 support, console only with open devtools
if (typeof console !== 'undefined') {
console.warn(text);
}

// NOTE: throw and catch immediately to provide a stack trace:
// https://developer.chrome.com/blog/automatically-pause-on-any-exception/
try {
throw Error(text);
// eslint-disable-next-line no-empty
} catch (x) {}
}
}
4 changes: 3 additions & 1 deletion src/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ const valuesByType = {
};
type ValueKey = keyof typeof valuesByType;

// https://developer.mozilla.org/en-US/docs/Glossary/Falsy
export const falsyValues = [false, 0, -0, '', null, undefined, NaN];
export const truthyValues = [true, 1, -1, 'test', {}, [], Number.POSITIVE_INFINITY];
// https://developer.mozilla.org/en-US/docs/Glossary/Truthy
export const truthyValues = [true, 1, -1, 'test', {}, []];

export function getValuesByType(keyOrKeys: ValueKey | ValueKey[]) {
if (Array.isArray(keyOrKeys)) {
Expand Down

1 comment on commit 810b5bf

@vercel
Copy link

@vercel vercel bot commented on 810b5bf Jul 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

emery – ./

emery-thinkmill.vercel.app
emery-git-main-thinkmill.vercel.app
emery-ts.vercel.app

Please sign in to comment.