Skip to content

Commit

Permalink
feat: remove useIf and improve error (#5)
Browse files Browse the repository at this point in the history
* feat: add NonNullish variations for core contracts

* docs: unify and fix new descriptions

* feat: add tests for non nullish contracts

* feat: remove useIf and fix error()

* refactor: remove unused generic

* docs: update README and documentation

* test: replace useIf with error in type tests

* ci: adjust coverage threshold

Co-authored-by: JanMalch <25508038+JanMalch@users.noreply.github.com>
  • Loading branch information
yoha-dev and JanMalch authored Oct 30, 2020
1 parent 236adb3 commit c4ceaf3
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 111 deletions.
37 changes: 5 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ You can now import the following functions `from 'ts-code-contracts'`:
- Utils
- [`error` to make code more concise](#error)
- [`isDefined` type guard](#isdefined)
- [`useIf` for assignments](#useif)

Make sure to read the `@example`s in the documentation below
or refer to the [test cases](https://github.com/JanMalch/ts-code-contracts/blob/master/index.test.ts#L166-L196)
and [typing assistance](https://github.com/JanMalch/ts-code-contracts/blob/master/index.test-d.ts#L54-L65)!
and [typing assistance](https://github.com/JanMalch/ts-code-contracts/blob/master/index.test-d.ts#L41-L52)!

## Contracts

Expand Down Expand Up @@ -80,7 +79,7 @@ export function requiresNonNullish<T>(
value: T,
message = 'Value must not be null or undefined'
): NonNullable<T>;
```
```

### `checks`

Expand Down Expand Up @@ -233,14 +232,14 @@ This function will always throw the given error and helps keeping code easy to r
* @see IllegalStateError
* @example
* function myFun(foo: string | null) {
* const bar: string = foo ?? error(PreconditionError, 'Argument may not be null');
* const bar = foo ?? error(PreconditionError, 'Argument may not be null');
* const result = bar.length > 0 ? 'OK' : error();
* }
*/
export function error<T>(
export function error(
errorType: new (...args: any[]) => Error = IllegalStateError,
message?: string
): T;
): never;
```

### `isDefined`
Expand All @@ -260,32 +259,6 @@ Make sure to use [`strictNullChecks`](https://basarat.gitbook.io/typescript/intr
export function isDefined<T>(value: T): value is NonNullable<T>;
```

### `useIf`

A function that helps with validating and typing when assigning variables.

```ts
/**
* Returns a function that will return the passed in value, if it passes the given predicate.
* If not, the given contract will throw an error with the given message.
* @param predicate the predicate that the value must pass
* @param contract the contract for context
* @param message the message for the contract
* @example
* function myFun(foo: string | null) {
* const bar = useIf(isDefined)(foo);
* }
*/
export function useIf<T, O extends T = T>(
predicate: (value: T | O) => value is O,
contract: (
condition: boolean,
message?: string
) => asserts condition = requires,
contractMessage?: string
): (value: T) => O;
```

## Errors

The following error classes are included:
Expand Down
17 changes: 2 additions & 15 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
ensures,
requires,
isDefined,
useIf,
error,
asserts,
unreachable,
Expand Down Expand Up @@ -35,20 +34,8 @@ function assertsExample(value: string | null) {
// UTILS

function errorExample(value: string | null) {
// error cannot help the compiler to infer the type
const result = value ?? error();
expectError<string>(result);
expectType<string | null>(result);
// to help the compiler, you can use it like this ...
const foo: string = value ?? error();
expectType<string>(foo);
// ... or like this
const bar = value ?? error<string>();
expectType<string>(bar);
}

function useIfExample(value: string | null) {
expectType<string>(useIf(isDefined)(value));
expectType<string>(result);
}

interface Named {
Expand All @@ -60,7 +47,7 @@ function isNamed(value: any): value is Named {
}

function useIfTypeGuardExample(value: any) {
const withName = useIf(isNamed)(value);
const withName = isNamed(value) ? value : error();
expectType<Named>(withName);
}

Expand Down
71 changes: 36 additions & 35 deletions index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
requires,
requiresNonNullish,
unreachable,
useIf,
} from './index';

describe('contracts', () => {
Expand Down Expand Up @@ -87,6 +86,42 @@ describe('utils', () => {
});
});

describe('NonNullish contract tests', () => {
const contractTest = (
contract: <T>(value: T, message?: string) => NonNullable<T>,
errorType: new (...args: any[]) => Error,
defaultMessage: string
): void => {
describe(contract.name, () => {
it('should not error if the value is defined', () => {
expect(() => contract('A nice String')).not.toThrowError();
});
it('should throw an Error if the value is not defined', () => {
expect(() => contract(null)).toThrowError(
// eslint-disable-next-line new-cap
new errorType(defaultMessage)
);
});
});
};

contractTest(
requiresNonNullish,
PreconditionError,
'Value must not be null or undefined'
);
contractTest(
checksNonNullish,
IllegalStateError,
'Value must not be null or undefined'
);
contractTest(
ensuresNonNullish,
PostconditionError,
'Value must not be null or undefined'
);
});

describe('isDefined', () => {
it('should return true for defined values', () => {
expect(isDefined('TypeScript')).toBe(true);
Expand All @@ -100,40 +135,6 @@ describe('utils', () => {
});
});

describe('useIf', () => {
interface Named {
name: string;
}

function isNamed(value: any): value is Named {
return value != null && typeof value.name === 'string';
}

it('should return the value if it passes the type guard', () => {
const input = { name: 'John' };
const ifIsNamed = useIf(isNamed);
expect(ifIsNamed(input)).toBe(input);
});
it('should throw a PreconditionError by default if the value does not pass the type guard', () => {
const ifIsNamed = useIf(isNamed);
expect(() => ifIsNamed(false)).toThrowError(
new PreconditionError('Unmet precondition')
);
});
it('should use the given contract', () => {
const ifIsNamed = useIf(isNamed, ensures);
expect(() => ifIsNamed(false)).toThrowError(
new PostconditionError('Unmet postcondition')
);
});
it('should use the given message for the contract error', () => {
const ifIsNamed = useIf(isNamed, ensures, 'Failed!');
expect(() => ifIsNamed(false)).toThrowError(
new PostconditionError('Failed!')
);
});
});

describe('unreachable', () => {
it('should always throw an error at runtime', () => {
expect(() => unreachable({} as never)).toThrowError();
Expand Down
31 changes: 3 additions & 28 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,31 +196,6 @@ export function isDefined<T>(value: T): value is NonNullable<T> {
return value != null;
}

/**
* Returns a function that will return the passed in value, if it passes the given type guard.
* If not, the given contract will throw an error with the given message.
* @param predicate the type guard that the value must pass
* @param contract the contract for context
* @param contractMessage the message for the contract
* @example
* function myFun(foo: string | null) {
* const bar = useIf(isDefined)(foo);
* }
*/
export function useIf<T, U extends T = T>(
predicate: (value: T | U) => value is U,
contract: (
condition: boolean,
message?: string
) => asserts condition = requires,
contractMessage?: string
): (value: T | U) => U {
return (value: T | U): U => {
contract(predicate(value), contractMessage);
return value;
};
}

/* eslint-disable @typescript-eslint/no-explicit-any, new-cap */

/**
Expand All @@ -231,14 +206,14 @@ export function useIf<T, U extends T = T>(
* @see AssertionError
* @example
* function myFun(foo: string | null) {
* const bar: string = foo ?? error(PreconditionError, 'Argument may not be null');
* const bar = foo ?? error(PreconditionError, 'Argument may not be null');
* const result = bar.length > 0 ? 'OK' : error('Something went wrong!');
* }
*/
export function error<T>(
export function error(
errorType: new (...args: any[]) => Error = AssertionError,
message?: string
): T {
): never {
throw new errorType(message);
}

Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module.exports = {
collectCoverage: true,
coverageThreshold: {
global: {
branches: 79, // bug: branch not covered on super call (https://stackoverflow.com/q/52820169)
branches: 78, // bug: branch not covered on super call (https://stackoverflow.com/q/52820169)
functions: 100,
lines: 100,
statements: 100,
Expand Down

0 comments on commit c4ceaf3

Please sign in to comment.