Skip to content

Commit

Permalink
Refactor: update some error messages and unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
alexismenest committed Jun 13, 2023
1 parent ad4b6a0 commit 7240bf9
Show file tree
Hide file tree
Showing 15 changed files with 117 additions and 121 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ Optional<TValue>
```
A container object which may or may not contain a non-`nullish` value. If a value is present, `isPresent()` returns `true`. If no value is present, the object is considered empty and `isPresent()` returns `false`.

Additional methods that depend on the presence or absence of a contained value are provided, such as `orElse()` (returns a default value if no value is present) and `ifPresent()` (performs an action if a value is present).

This is a value-based class; instances that are equal should be treated as interchangeable.

`Optional` is primarily intended for use as a method return type where there is a clear need to represent "no result", and where using `null` or `undefined` is likely to cause errors. A variable whose type is `Optional` should never itself be `null` or `undefined`; it should always point to an `Optional` instance.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "neatutil.optional",
"version": "1.0.0",
"version": "1.0.2",
"description": "A TypeScript isomorphic adaptation of Java's Optional.",
"author": "Alexis Loïc A. Menest <[email protected]>",
"license": "MIT",
Expand Down
60 changes: 29 additions & 31 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import isEqual from 'lodash.isequal';

const ACTION = '"action"';
const EMPTY_ACTION = '"emptyAction"';
const ACTION_PARAMETER = '"action"';
const EMPTY_ACTION_PARAMETER = '"emptyAction"';
const EXPECTED = '; expected: ';
const ERROR = '"Error"';
const ERROR_SUPPLIER = '"errorSupplier"';
const FUNCTION = '"function"';
const INVALID = 'invalid';
const MAPPER = '"mapper"';
const NON_NULLISH = '"non-nullish"';
const OPTIONAL = '"Optional"';
const OPTIONAL_TS = 'Optional';
const ERROR_TYPE = '"Error"';
const ERROR_SUPPLIER_PARAMETER = '"errorSupplier"';
const FUNCTION_TYPE = '"function"';
const INVALID_ARGUMENT_TYPE_FOR_PARAMETER = 'invalid argument type for parameter ';
const INVALID_RETURN_TYPE_FOR_FUNCTION = 'invalid return type for function ';
const MAPPER_PARAMETER = '"mapper"';
const NON_NULLISH_TYPE = '"non-nullish"';
const OPTIONAL = 'Optional';
const OPTIONAL_TYPE = '"Optional"';
const OPTIONAL_EMPTY = 'Optional.empty';
const PREDICATE = '"predicate"';
const SUPPLIER = '"supplier"';
const TYPE_FOR = 'type for';
const VALUE = '"value"';
const PREDICATE_PARAMETER = '"predicate"';
const SUPPLIER_PARAMETER = '"supplier"';
const VALUE_PARAMETER = '"value"';
const VALUE_NOT_PRESENT = 'value not present';
const INVALID_ARGUMENT_TYPE_FOR_PARAMETER = `${INVALID} argument ${TYPE_FOR} parameter `;
const INVALID_RETURN_TYPE_FOR_METHOD = `${INVALID} return ${TYPE_FOR} method `;

const isFunction = (value: unknown): boolean => typeof value === 'function';

Expand All @@ -28,7 +26,7 @@ const invalidArgTypeErrorMessage = (strings: TemplateStringsArray, paramNameExp:
`${INVALID_ARGUMENT_TYPE_FOR_PARAMETER}${paramNameExp}${EXPECTED}${typeExp}`;

const invalidReturnTypeErrorMessage = (strings: TemplateStringsArray, funcNameExp: string, typeExp: string): string =>
`${INVALID_RETURN_TYPE_FOR_METHOD}${funcNameExp}${EXPECTED}${typeExp}`;
`${INVALID_RETURN_TYPE_FOR_FUNCTION}${funcNameExp}${EXPECTED}${typeExp}`;

/**
* A container object which may or may not contain a non-`nullish` value.
Expand Down Expand Up @@ -70,7 +68,7 @@ export class Optional<TValue> {
*/
public static of<TValue>(value: TValue): Optional<TValue> {
if (isNullish(value) === true) {
throw new TypeError(invalidArgTypeErrorMessage`${VALUE}${NON_NULLISH}`);
throw new TypeError(invalidArgTypeErrorMessage`${VALUE_PARAMETER}${NON_NULLISH_TYPE}`);
}

return new Optional<TValue>(value);
Expand Down Expand Up @@ -118,7 +116,7 @@ export class Optional<TValue> {
*/
public filter(predicate: (value: TValue) => boolean): Optional<TValue> {
if (isFunction(predicate) === false) {
throw new TypeError(invalidArgTypeErrorMessage`${PREDICATE}${FUNCTION}`);
throw new TypeError(invalidArgTypeErrorMessage`${PREDICATE_PARAMETER}${FUNCTION_TYPE}`);
}

if ((this.isPresent() === true) && (predicate(this.#value as TValue) === true)) {
Expand All @@ -140,14 +138,14 @@ export class Optional<TValue> {
*/
public flatMap<UValue>(mapper: (value: TValue) => Optional<UValue>): Optional<UValue> {
if (isFunction(mapper) === false) {
throw new TypeError(invalidArgTypeErrorMessage`${MAPPER}${FUNCTION}`);
throw new TypeError(invalidArgTypeErrorMessage`${MAPPER_PARAMETER}${FUNCTION_TYPE}`);
}

if (this.isPresent() === true) {
const mappingResult = mapper(this.#value as TValue);

if ((mappingResult instanceof Optional) === false) {
throw new TypeError(invalidReturnTypeErrorMessage`${MAPPER}${OPTIONAL}`);
throw new TypeError(invalidReturnTypeErrorMessage`${MAPPER_PARAMETER}${OPTIONAL_TYPE}`);
}

return mappingResult;
Expand Down Expand Up @@ -178,7 +176,7 @@ export class Optional<TValue> {
*/
public ifPresent(action: (value: TValue) => void): void {
if (isFunction(action) === false) {
throw new TypeError(invalidArgTypeErrorMessage`${ACTION}${FUNCTION}`);
throw new TypeError(invalidArgTypeErrorMessage`${ACTION_PARAMETER}${FUNCTION_TYPE}`);
}

if (this.isPresent() === true) {
Expand All @@ -196,11 +194,11 @@ export class Optional<TValue> {
*/
public ifPresentOrElse(action: (value: TValue) => void, emptyAction: () => void): void {
if (isFunction(action) === false) {
throw new TypeError(invalidArgTypeErrorMessage`${ACTION}${FUNCTION}`);
throw new TypeError(invalidArgTypeErrorMessage`${ACTION_PARAMETER}${FUNCTION_TYPE}`);
}

if (isFunction(emptyAction) === false) {
throw new TypeError(invalidArgTypeErrorMessage`${EMPTY_ACTION}${FUNCTION}`);
throw new TypeError(invalidArgTypeErrorMessage`${EMPTY_ACTION_PARAMETER}${FUNCTION_TYPE}`);
}

if (this.isPresent() === true) {
Expand Down Expand Up @@ -241,7 +239,7 @@ export class Optional<TValue> {
*/
public map<UValue>(mapper: (value: TValue) => UValue): Optional<UValue> {
if (isFunction(mapper) === false) {
throw new TypeError(invalidArgTypeErrorMessage`${MAPPER}${FUNCTION}`);
throw new TypeError(invalidArgTypeErrorMessage`${MAPPER_PARAMETER}${FUNCTION_TYPE}`);
}

if (this.isPresent() === true) {
Expand All @@ -262,7 +260,7 @@ export class Optional<TValue> {
*/
public or(supplier: () => Optional<TValue>): Optional<TValue> {
if (isFunction(supplier) === false) {
throw new TypeError(invalidArgTypeErrorMessage`${SUPPLIER}${FUNCTION}`);
throw new TypeError(invalidArgTypeErrorMessage`${SUPPLIER_PARAMETER}${FUNCTION_TYPE}`);
}

if (this.isPresent() === true) {
Expand All @@ -272,7 +270,7 @@ export class Optional<TValue> {
const supplyingResult = supplier();

if ((supplyingResult instanceof Optional) === false) {
throw new TypeError(invalidReturnTypeErrorMessage`${SUPPLIER}${OPTIONAL}`);
throw new TypeError(invalidReturnTypeErrorMessage`${SUPPLIER_PARAMETER}${OPTIONAL_TYPE}`);
}

return supplyingResult;
Expand All @@ -297,7 +295,7 @@ export class Optional<TValue> {
*/
public orElseGet(supplier: () => TValue | null): TValue | null {
if (isFunction(supplier) === false) {
throw new TypeError(invalidArgTypeErrorMessage`${SUPPLIER}${FUNCTION}`);
throw new TypeError(invalidArgTypeErrorMessage`${SUPPLIER_PARAMETER}${FUNCTION_TYPE}`);
}

if (this.isPresent() === true) {
Expand Down Expand Up @@ -334,14 +332,14 @@ export class Optional<TValue> {
*/
public orElseGetThrow<TError extends Error>(errorSupplier: () => TError): TValue {
if (isFunction(errorSupplier) === false) {
throw new TypeError(invalidArgTypeErrorMessage`${ERROR_SUPPLIER}${FUNCTION}`);
throw new TypeError(invalidArgTypeErrorMessage`${ERROR_SUPPLIER_PARAMETER}${FUNCTION_TYPE}`);
}

if (this.isPresent() === false) {
const supplyingResult = errorSupplier();

if ((supplyingResult instanceof Error) === false) {
throw new TypeError(invalidReturnTypeErrorMessage`${ERROR_SUPPLIER}${ERROR}`);
throw new TypeError(invalidReturnTypeErrorMessage`${ERROR_SUPPLIER_PARAMETER}${ERROR_TYPE}`);
}

throw supplyingResult;
Expand All @@ -356,6 +354,6 @@ export class Optional<TValue> {
* @returns {string} The `string` representation of this instance.
*/
public toString(): string {
return this.isPresent() === true ? `${OPTIONAL_TS}[${this.#value}]` : OPTIONAL_EMPTY;
return this.isPresent() === true ? `${OPTIONAL}[${this.#value}]` : OPTIONAL_EMPTY;
}
}
8 changes: 4 additions & 4 deletions test/equals.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ describe('instance method "equals"', () => {

expect(emptyInstance.equals(firstNonEmptyInstance)).toBe(false);
expect(emptyInstance.equals(secondNonEmptyInstance)).toBe(false);
// @ts-ignore
// @ts-ignore: Testing wrong argument type
expect(firstNonEmptyInstance.equals(emptyInstance)).toBe(false);
// @ts-ignore
// @ts-ignore: Testing wrong argument type
expect(firstNonEmptyInstance.equals(secondNonEmptyInstance)).toBe(false);
// @ts-ignore
// @ts-ignore: Testing wrong argument type
expect(secondNonEmptyInstance.equals(emptyInstance)).toBe(false);
// @ts-ignore
// @ts-ignore: Testing wrong argument type
expect(secondNonEmptyInstance.equals(firstNonEmptyInstance)).toBe(false);
});

Expand Down
14 changes: 7 additions & 7 deletions test/filter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,21 @@ describe('instance method "filter"', () => {
test('calling with a non-function argument throws', () => {
const nullArg = null;
const objectArg = {};
const error = new TypeError('invalid argument type for parameter "predicate"; expected: "function"');
const errorThrown = new TypeError('invalid argument type for parameter "predicate"; expected: "function"');
const emptyInstance = Optional.empty();
const nonEmptyInstance = Optional.of<string>('');

// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.filter()).toThrow(error);
expect(() => emptyInstance.filter()).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.filter(nullArg)).toThrow(error);
expect(() => emptyInstance.filter(nullArg)).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.filter(objectArg)).toThrow(error);
expect(() => emptyInstance.filter(objectArg)).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.filter()).toThrow(error);
expect(() => nonEmptyInstance.filter()).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.filter(nullArg)).toThrow(error);
expect(() => nonEmptyInstance.filter(nullArg)).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.filter(objectArg)).toThrow(error);
expect(() => nonEmptyInstance.filter(objectArg)).toThrow(errorThrown);
});
});
20 changes: 10 additions & 10 deletions test/flatMap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,31 @@ describe('instance method "flatMap"', () => {
test('calling with a non-function argument throws', () => {
const nullArg = null;
const objectArg = {};
const error = new TypeError('invalid argument type for parameter "mapper"; expected: "function"');
const errorThrown = new TypeError('invalid argument type for parameter "mapper"; expected: "function"');
const emptyInstance = Optional.empty();
const nonEmptyInstance = Optional.of<string>('');
const nonEmptyInstance = Optional.of<number>(5);

// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.flatMap()).toThrow(error);
expect(() => emptyInstance.flatMap()).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.flatMap(nullArg)).toThrow(error);
expect(() => emptyInstance.flatMap(nullArg)).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.flatMap(objectArg)).toThrow(error);
expect(() => emptyInstance.flatMap(objectArg)).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.flatMap<string>()).toThrow(error);
expect(() => nonEmptyInstance.flatMap<string>()).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.flatMap<string>(nullArg)).toThrow(error);
expect(() => nonEmptyInstance.flatMap<string>(nullArg)).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.flatMap<string>(objectArg)).toThrow(error);
expect(() => nonEmptyInstance.flatMap<string>(objectArg)).toThrow(errorThrown);
});

test('calling on an non-empty instance with a function argument that does not return an "Optional" instance throws', () => {
const error = new TypeError('invalid return type for method "mapper"; expected: "Optional"');
const errorThrown = new TypeError('invalid return type for function "mapper"; expected: "Optional"');
const nonOptionalReturningMapper = jest.fn((value: number) => `give me ${value}`);
const nonEmptyInstance = Optional.of<number>(5);

// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.flatMap<string>(nonOptionalReturningMapper)).toThrow(error);
expect(() => nonEmptyInstance.flatMap<string>(nonOptionalReturningMapper)).toThrow(errorThrown);
expect(nonOptionalReturningMapper).toHaveBeenCalledTimes(1);
expect(nonOptionalReturningMapper).toHaveBeenCalledWith(5);
expect(nonOptionalReturningMapper).toHaveReturnedWith('give me 5');
Expand Down
4 changes: 2 additions & 2 deletions test/get.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ describe('instance method "get"', () => {
});

test('calling on an empty instance throws', () => {
const error = new Error('value not present');
const errorThrown = new Error('value not present');
const emptyInstance = Optional.empty();

expect(() => emptyInstance.get()).toThrow(error);
expect(() => emptyInstance.get()).toThrow(errorThrown);
});
});
14 changes: 7 additions & 7 deletions test/ifPresent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@ describe('instance method "ifPresent"', () => {
test('calling with a non-function argument throws', () => {
const nullArg = null;
const objectArg = {};
const error = new TypeError('invalid argument type for parameter "action"; expected: "function"');
const errorThrown = new TypeError('invalid argument type for parameter "action"; expected: "function"');
const emptyInstance = Optional.empty();
const nonEmptyInstance = Optional.of<string>('');

// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresent()).toThrow(error);
expect(() => emptyInstance.ifPresent()).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresent(nullArg)).toThrow(error);
expect(() => emptyInstance.ifPresent(nullArg)).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresent(objectArg)).toThrow(error);
expect(() => emptyInstance.ifPresent(objectArg)).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresent()).toThrow(error);
expect(() => nonEmptyInstance.ifPresent()).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresent(nullArg)).toThrow(error);
expect(() => nonEmptyInstance.ifPresent(nullArg)).toThrow(errorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresent(objectArg)).toThrow(error);
expect(() => nonEmptyInstance.ifPresent(objectArg)).toThrow(errorThrown);
});
});
36 changes: 18 additions & 18 deletions test/ifPresentOrElse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,42 +39,42 @@ describe('instance method "ifPresentOrElse"', () => {
const emptyAction = () => { console.log(); };
const nullArg = null;
const objectArg = {};
const actionError = new TypeError('invalid argument type for parameter "action"; expected: "function"');
const emptyActionError = new TypeError('invalid argument type for parameter "emptyAction"; expected: "function"');
const actionErrorThrown = new TypeError('invalid argument type for parameter "action"; expected: "function"');
const emptyActionErrorThrown = new TypeError('invalid argument type for parameter "emptyAction"; expected: "function"');
const emptyInstance = Optional.empty();
const nonEmptyInstance = Optional.of<string>('');

// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresentOrElse()).toThrow(actionError);
expect(() => emptyInstance.ifPresentOrElse()).toThrow(actionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresentOrElse(nullArg)).toThrow(actionError);
expect(() => emptyInstance.ifPresentOrElse(nullArg)).toThrow(actionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresentOrElse(nullArg, emptyAction)).toThrow(actionError);
expect(() => emptyInstance.ifPresentOrElse(nullArg, emptyAction)).toThrow(actionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresentOrElse(objectArg)).toThrow(actionError);
expect(() => emptyInstance.ifPresentOrElse(objectArg)).toThrow(actionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresentOrElse(objectArg, emptyAction)).toThrow(actionError);
expect(() => emptyInstance.ifPresentOrElse(objectArg, emptyAction)).toThrow(actionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresentOrElse(action)).toThrow(emptyActionError);
expect(() => emptyInstance.ifPresentOrElse(action)).toThrow(emptyActionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresentOrElse(action, nullArg)).toThrow(emptyActionError);
expect(() => emptyInstance.ifPresentOrElse(action, nullArg)).toThrow(emptyActionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => emptyInstance.ifPresentOrElse(action, objectArg)).toThrow(emptyActionError);
expect(() => emptyInstance.ifPresentOrElse(action, objectArg)).toThrow(emptyActionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresentOrElse()).toThrow(actionError);
expect(() => nonEmptyInstance.ifPresentOrElse()).toThrow(actionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresentOrElse(nullArg)).toThrow(actionError);
expect(() => nonEmptyInstance.ifPresentOrElse(nullArg)).toThrow(actionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresentOrElse(nullArg, emptyAction)).toThrow(actionError);
expect(() => nonEmptyInstance.ifPresentOrElse(nullArg, emptyAction)).toThrow(actionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresentOrElse(objectArg)).toThrow(actionError);
expect(() => nonEmptyInstance.ifPresentOrElse(objectArg)).toThrow(actionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresentOrElse(objectArg, emptyAction)).toThrow(actionError);
expect(() => nonEmptyInstance.ifPresentOrElse(objectArg, emptyAction)).toThrow(actionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresentOrElse(action)).toThrow(emptyActionError);
expect(() => nonEmptyInstance.ifPresentOrElse(action)).toThrow(emptyActionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresentOrElse(action, nullArg)).toThrow(emptyActionError);
expect(() => nonEmptyInstance.ifPresentOrElse(action, nullArg)).toThrow(emptyActionErrorThrown);
// @ts-expect-error: Testing wrong argument type
expect(() => nonEmptyInstance.ifPresentOrElse(action, objectArg)).toThrow(emptyActionError);
expect(() => nonEmptyInstance.ifPresentOrElse(action, objectArg)).toThrow(emptyActionErrorThrown);
});
});
Loading

0 comments on commit 7240bf9

Please sign in to comment.