Skip to content

Commit

Permalink
fix: Restructure type hierarchy so that types don't conflict internal…
Browse files Browse the repository at this point in the history
…ly. (#330)

The old `Result<TValue, TError> = ResultValue<TValue> | ResultError<TError>`
lead to the expansion `ResultBase<TValue, Error> | ResultBase<unknown, TError>`
which lead to a conflict on the `unwrapOrThrow` method and others, where
the return type was suddenly `TValue | unknown` and thus always unknown.

This commit restructures the type hierarchy so that this conflict is
avoided.
  • Loading branch information
Hannes Leutloff authored Jul 12, 2021
1 parent d028d18 commit 610302d
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 24 deletions.
18 changes: 9 additions & 9 deletions lib/Result.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
interface ResultBase<TValue, TError extends Error> {
hasError: () => this is ResultError<TError>;
hasValue: () => this is ResultValue<TValue>;
hasError: () => this is ResultError<TError> & ResultBase<any, TError>;
hasValue: () => this is ResultValue<TValue> & ResultBase<TValue, any>;

unwrapOrThrow: (errorTransformer?: (err: TError) => Error) => TValue;
unwrapOrElse: (handleError: (error: Error) => TValue) => TValue;
unwrapOrDefault: (defaultValue: TValue) => TValue;
}

interface ResultError<TError extends Error> extends ResultBase<unknown, TError> {
interface ResultError<TError extends Error> {
error: TError;
}

const error = function <TError extends Error>(err: TError): ResultError<TError> {
const error = function <TError extends Error>(err: TError): ResultError<TError> & ResultBase<any, TError> {
return {
hasError (): boolean {
return true;
Expand All @@ -36,14 +36,14 @@ const error = function <TError extends Error>(err: TError): ResultError<TError>
};
};

interface ResultValue<TValue> extends ResultBase<TValue, Error> {
interface ResultValue<TValue> {
value: TValue;
}

const value: {
<TValue extends undefined>(): ResultValue<TValue>;
<TValue>(value: TValue): ResultValue<TValue>;
} = function <TValue>(val?: TValue): ResultValue<TValue | undefined> {
<TValue extends undefined>(): ResultValue<TValue> & ResultBase<TValue, any>;
<TValue>(value: TValue): ResultValue<TValue> & ResultBase<TValue, any>;
} = function <TValue>(val?: TValue): ResultValue<TValue | undefined> & ResultBase<TValue | undefined, any> {
return {
hasError (): boolean {
return false;
Expand All @@ -64,7 +64,7 @@ const value: {
};
};

type Result<TValue, TError extends Error> = ResultValue<TValue> | ResultError<TError>;
type Result<TValue, TError extends Error> = ResultBase<TValue, TError>;

export type {
ResultValue,
Expand Down
62 changes: 47 additions & 15 deletions test/unit/ResultTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import { assert } from 'assertthat';
import { defekt } from 'lib';
import { error, Result, value } from '../../lib/Result';

interface Value {
foo: string;
}

const getValue = function (): Result<Value, Error> {
return value({ foo: 'bar' });
};
const getError = function (): Result<Value, Error> {
// eslint-disable-next-line unicorn/error-message
return error(new Error());
};

suite('Result', (): void => {
suite('error', (): void => {
test('constructs the ResultError type.', async (): Promise<void> => {
Expand Down Expand Up @@ -49,49 +61,69 @@ suite('Result', (): void => {

suite('hasError', (): void => {
test(`returns true for something constructed with 'error()'.`, async (): Promise<void> => {
// eslint-disable-next-line unicorn/error-message
const result = error(new Error());
const result = getError();

const resultHasError = result.hasError();

assert.that(resultHasError).is.true();
});

test(`returns false for something constructed with 'value()'.`, async (): Promise<void> => {
const result = value({ foo: 'bar' });
const result = getValue();

const resultHasError = result.hasError();

assert.that(resultHasError).is.false();
});

test(`narrows the type to the error case and discards value type information.`, async (): Promise<void> => {
const result = getValue();

if (result.hasError()) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const newResult: Result<{ something: 'elseEntirely' }, Error> = result;
}
});
});

suite('hasValue', (): void => {
test(`returns true for something constructed with 'value()'.`, async (): Promise<void> => {
const result = value({ foo: 'bar' });
const result = getValue();

const resultHasValue = result.hasValue();

assert.that(resultHasValue).is.true();
});

test(`returns false for something constructed with 'error()'.`, async (): Promise<void> => {
// eslint-disable-next-line unicorn/error-message
const result = error(new Error());
const result = getError();

const resultHasValue = result.hasValue();

assert.that(resultHasValue).is.false();
});

test(`narrows the type to the value case and discards error type information.`, async (): Promise<void> => {
const result = getValue();

interface CustomError extends Error {
bar: string;
}

if (result.hasValue()) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const newResult: Result<Value, CustomError> = result;
}
});
});

suite('unwrapOrThrow', (): void => {
test('unwraps the result if it does not have an error.', async (): Promise<void> => {
test('unwraps the result with the correct value type if it does not have an error.', async (): Promise<void> => {
const val = { foo: 'bar' };

const result = value(val);
const result = value(val) as Result<Value, Error>;

const unwrappedResult = result.unwrapOrThrow();
const unwrappedResult: Value = result.unwrapOrThrow();

assert.that(unwrappedResult).is.equalTo(val);
});
Expand Down Expand Up @@ -147,9 +179,9 @@ suite('Result', (): void => {
const val = { foo: 'bar' };
const errorHandler = (): { foo: string } => val;

const result = error(err);
const result = error(err) as Result<Value, Error>;

const unwrappedResult = result.unwrapOrElse(errorHandler);
const unwrappedResult: Value = result.unwrapOrElse(errorHandler);

assert.that(unwrappedResult).is.equalTo(val);
});
Expand All @@ -160,9 +192,9 @@ suite('Result', (): void => {
const val = { foo: 'bar' };
const defaultValue = { foo: 'not-bar' };

const result = value(val);
const result = value(val) as Result<Value, Error>;

const unwrappedResult = result.unwrapOrDefault(defaultValue);
const unwrappedResult: Value = result.unwrapOrDefault(defaultValue);

assert.that(unwrappedResult).is.equalTo(val);
});
Expand All @@ -172,9 +204,9 @@ suite('Result', (): void => {
const err = new Error();
const defaultValue = { foo: 'bar' };

const result = error(err);
const result = error(err) as Result<Value, Error>;

const unwrappedResult = result.unwrapOrDefault(defaultValue);
const unwrappedResult: Value = result.unwrapOrDefault(defaultValue);

assert.that(unwrappedResult).is.equalTo(defaultValue);
});
Expand Down

0 comments on commit 610302d

Please sign in to comment.