Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["node_modules", "dist", "build", ".turbo", "*.config.js"],
ignores: ["node_modules", "dist", "build", ".turbo", "*.config.js", "**/*.test-d.ts"],
},
{
files: ["**/*.ts", "**/*.tsx"],
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@deessejs/type-testing": "^0.3.4",
"@vitest/coverage-v8": "^2.0.0",
"tsup": "^8.5.1",
"typescript": "^5.0.0",
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/result/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,11 @@ export const match = <T, E extends Error, U>(
* @typeParam E - The type of the error
* @param result - The Result to swap
* @returns Err if Ok, Ok if Err
* @note This function has type limitations with E extends Error constraint
* @note This function has type limitations because:
* - Ok<T, E> swapped becomes Err<T>, but Err requires its type to extend Error
* - Err<E> swapped becomes Ok<E>, but Ok's second param must extend Error
* TypeScript cannot express "T becomes the error" or "E becomes the value"
* without circular type constraints. The any is unavoidable for this operation.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const swap = (result: any): any =>
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export const retryAsync = async <T>(fn: () => Promise<T>, options: RetryOptions
}
}

// Unreachable - TypeScript safety
// istanbul ignore next - TypeScript exhaustive check, unreachable at runtime
throw lastError!;
};

Expand Down Expand Up @@ -199,6 +199,6 @@ export const retry = <T>(fn: () => T, options: RetryOptions = {}): T => {
}
}

// Unreachable - TypeScript safety
// istanbul ignore next - TypeScript exhaustive check, unreachable at runtime
throw lastError!;
};
137 changes: 137 additions & 0 deletions packages/core/tests/types/async-result.types.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Type tests for AsyncResult types
* These tests verify compile-time type relationships using @deessejs/type-testing
*/

import { check, HasProperty } from "@deessejs/type-testing";
import type { AsyncResult, AsyncOk, AsyncErr, AsyncResultInner, AbortError } from "../../src/async-result/types";
import type { Error } from "../../src/error/types";

// =============================================================================
// AsyncOk Type
// =============================================================================

// AsyncOk should have ok: true
check<AsyncOk<number>["ok"]>().equals<true>();

// AsyncOk should have value: T
check<AsyncOk<number>["value"]>().equals<number>();

// =============================================================================
// AsyncErr Type
// =============================================================================

// AsyncErr should have ok: false
check<AsyncErr<Error>["ok"]>().equals<false>();

// AsyncErr should have error: E
check<AsyncErr<Error>["error"]>().equals<Error>();

// =============================================================================
// AsyncResultInner Type (union)
// =============================================================================

// AsyncResultInner<T, E> should be a union of AsyncOk<T> and AsyncErr<E>
check<AsyncResultInner<number, Error>>().equals<AsyncOk<number> | AsyncErr<Error>>();

// =============================================================================
// AsyncResult Interface
// =============================================================================

// AsyncResult should have ok: boolean | undefined (before resolution it's undefined)
check<AsyncResult<number, Error>["ok"]>().equals<boolean | undefined>();

// AsyncResult should have value: T
check<HasProperty<AsyncResult<number, Error>, "value">>().equals<true>();

// AsyncResult should have error: E
check<HasProperty<AsyncResult<number, Error>, "error">>().equals<true>();

// =============================================================================
// AsyncResult.then method
// =============================================================================

// AsyncResult.then should have correct signature
type ThenFn = (onfulfilled?: (value: AsyncResultInner<number, Error>) => unknown, onrejected?: (reason: Error) => unknown) => Promise<unknown>;
check<AsyncResult<number, Error>["then"]>().equals<ThenFn>();

// =============================================================================
// AsyncResult.map method
// =============================================================================

// AsyncResult.map should return AsyncResult<U, E>
check<ReturnType<AsyncResult<number, Error>["map"]>>().equals<AsyncResult<string, Error>>();

// =============================================================================
// AsyncResult.mapErr method
// =============================================================================

// AsyncResult.mapErr should return AsyncResult<T, F>
check<ReturnType<AsyncResult<number, Error>["mapErr"]>>().equals<AsyncResult<number, Error>>();

// =============================================================================
// AsyncResult.flatMap method
// =============================================================================

// AsyncResult.flatMap should return AsyncResult<U, E>
check<ReturnType<AsyncResult<number, Error>["flatMap"]>>().equals<AsyncResult<string, Error>>();

// =============================================================================
// AsyncResult.getOrElse method
// =============================================================================

// AsyncResult.getOrElse should return Promise<T>
check<ReturnType<AsyncResult<number, Error>["getOrElse"]>>().equals<Promise<number>>();

// =============================================================================
// AsyncResult.match method
// =============================================================================

// AsyncResult.match should return Promise<U>
check<ReturnType<AsyncResult<number, Error>["match"]>>().equals<Promise<string>>();

// =============================================================================
// AsyncResult.toNullable method
// =============================================================================

// AsyncResult.toNullable should return Promise<T | null>
check<ReturnType<AsyncResult<number, Error>["toNullable"]>>().equals<Promise<number | null>>();

// =============================================================================
// AsyncResult.toUndefined method
// =============================================================================

// AsyncResult.toUndefined should return Promise<T | undefined>
check<ReturnType<AsyncResult<number, Error>["toUndefined"]>>().equals<Promise<number | undefined>>();

// =============================================================================
// AsyncResult.unwrap method
// =============================================================================

// AsyncResult.unwrap should return Promise<T>
check<ReturnType<AsyncResult<number, Error>["unwrap"]>>().equals<Promise<number>>();

// =============================================================================
// AsyncResult.unwrapOr method
// =============================================================================

// AsyncResult.unwrapOr should return Promise<T>
check<ReturnType<AsyncResult<number, Error>["unwrapOr"]>>().equals<Promise<number>>();

// =============================================================================
// AsyncResult.toPromise method
// =============================================================================

// AsyncResult.toPromise should return Promise<AsyncResultInner<T, E>>
check<ReturnType<AsyncResult<number, Error>["toPromise"]>>().equals<Promise<AsyncResultInner<number, Error>>>();

// =============================================================================
// AbortError Type
// =============================================================================

// AbortError should extend Error
type AbortErrorExtendsError = AbortError extends Error ? true : false;
check<AbortErrorExtendsError>().equals<true>();

// AbortError should have name: "AbortError"
check<AbortError["name"]>().equals<"AbortError">;
105 changes: 105 additions & 0 deletions packages/core/tests/types/error.types.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Type tests for Error types
* These tests verify compile-time type relationships using @deessejs/type-testing
*/

import { check, HasProperty, PropertyType } from "@deessejs/type-testing";
import type { Error, ErrorData, ErrorMethods, ErrorBuilder, ExtractError, NativeError, ErrorGroup, ErrorOptions } from "../../src/error/types";
import type { Maybe } from "../../src/maybe/types";

// =============================================================================
// NativeError Type
// =============================================================================

// NativeError should be an alias for globalThis.Error
check<NativeError>().equals<globalThis.Error>();

// =============================================================================
// ErrorData Type
// =============================================================================

// ErrorData should have name: string
check<HasProperty<ErrorData<unknown>, "name">>().equals<true>();
check<PropertyType<ErrorData<unknown>, "name">>().equals<string>();

// ErrorData should have args: T
check<HasProperty<ErrorData<{ code: string }>, "args">>().equals<true>();
check<PropertyType<ErrorData<{ code: string }>, "args">>().equals<{ readonly code: string }>();

// ErrorData should have notes: readonly string[]
check<HasProperty<ErrorData<unknown>, "notes">>().equals<true>();
check<PropertyType<ErrorData<unknown>, "notes">>().equals<readonly string[]>();

// ErrorData should have cause: Maybe<NativeError>
check<HasProperty<ErrorData<unknown>, "cause">>().equals<true>();
check<PropertyType<ErrorData<unknown>, "cause">>().equals<Maybe<NativeError>>();

// ErrorData should have message: string
check<HasProperty<ErrorData<unknown>, "message">>().equals<true>();

// ErrorData should have optional stack
check<HasProperty<ErrorData<unknown>, "stack">>().equals<true>();

// =============================================================================
// Error Type (intersection)
// =============================================================================

// Error<T> = Readonly<ErrorData<T>> & NativeError & ErrorMethods<T>
type ErrorTypeForCheck = Error<{ code: string }>;
type ExpectedErrorIntersection = Readonly<ErrorData<{ code: string }>> & globalThis.Error & ErrorMethods<{ code: string }>;
check<ErrorTypeForCheck>().equals<ExpectedErrorIntersection>();

// Error should have all ErrorData properties
check<HasProperty<Error<unknown>, "name">>().equals<true>();
check<HasProperty<Error<unknown>, "args">>().equals<true>();
check<HasProperty<Error<unknown>, "notes">>().equals<true>();
check<HasProperty<Error<unknown>, "cause">>().equals<true>();
check<HasProperty<Error<unknown>, "message">>().equals<true>();

// =============================================================================
// ErrorMethods Type
// =============================================================================

// ErrorMethods should have addNotes that returns Error<T>
type AddNotesFn = (...notes: string[]) => Error<unknown>;
check<ErrorMethods<unknown>["addNotes"]>().equals<AddNotesFn>();

// ErrorMethods should have from that accepts Error or Maybe<Error>
type FromFn = (cause: Error | Maybe<Error>) => Error<unknown>;
check<ErrorMethods<unknown>["from"]>().equals<FromFn>();

// =============================================================================
// ErrorBuilder Type
// =============================================================================

// ErrorBuilder<T> = (args?: T) => Error<T>
type ErrorBuilderFn = (_: { code: string }) => Error<{ code: string }>;
check<ErrorBuilder<{ code: string }>>().equals<ErrorBuilderFn>();

// =============================================================================
// ExtractError Type (conditional type)
// =============================================================================

// ExtractError should extract Error<E> from ErrorBuilder<E>
check<ExtractError<ErrorBuilder<{ code: string }>>>().equals<Error<{ code: string }>>();

// =============================================================================
// ErrorGroup Type
// =============================================================================

// ErrorGroup should have exceptions property
check<HasProperty<ErrorGroup, "exceptions">>().equals<true>();
check<PropertyType<ErrorGroup, "exceptions">>().equals<readonly Error[]>();

// =============================================================================
// ErrorOptions Type (for error factory)
// =============================================================================

// ErrorOptions should have name
check<HasProperty<ErrorOptions<unknown>, "name">>().equals<true>();

// ErrorOptions should have optional schema
check<HasProperty<ErrorOptions<unknown>, "schema">>().equals<true>();

// ErrorOptions should have optional message
check<HasProperty<ErrorOptions<unknown>, "message">>().equals<true>();
102 changes: 102 additions & 0 deletions packages/core/tests/types/maybe.types.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Type tests for Maybe types
* These tests verify compile-time type relationships using @deessejs/type-testing
*/

import { check } from "@deessejs/type-testing";
import type { Maybe, Some, None } from "../../src/maybe/types";
import type { Result } from "../../src/result/types";
import type { Error } from "../../src/error/types";

// =============================================================================
// Maybe Type Definition
// =============================================================================

// Maybe<T> should be a union of Some<T> and None
check<Maybe<number>>().equals<Some<number> | None>();

// =============================================================================
// Some Type
// =============================================================================

// Some should have ok: true
check<Some<number>["ok"]>().equals<true>();

// Some should have value: T
check<Some<number>["value"]>().equals<number>();

// Some.isSome() should return true
check<ReturnType<Some<number>["isSome"]>>().equals<true>();

// Some.isNone() should return false
check<ReturnType<Some<number>["isNone"]>>().equals<false>();

// Some.equals should accept Maybe<T>
type SomeEqualsFn = (other: Maybe<number>) => boolean;
check<Some<number>["equals"]>().equals<SomeEqualsFn>();

// Some.equals with comparator should work
type SomeEqualsWithComparatorFn = (other: Maybe<number>, comparator: (a: number, b: number) => boolean) => boolean;
check<Some<number>["equals"]>().equals<SomeEqualsWithComparatorFn>();

// Some.filter should return Maybe<T>
check<ReturnType<Some<number>["filter"]>>().equals<Maybe<number>>();

// Some.map should return Maybe<U>
check<ReturnType<Some<number>["map"]>>().equals<Maybe<string>>();

// Some.flatMap should return Maybe<U>
check<ReturnType<Some<number>["flatMap"]>>().equals<Maybe<string>>();

// Some.getOrElse should return T
check<ReturnType<Some<number>["getOrElse"]>>().equals<number>();

// Some.getOrCompute should return T
check<ReturnType<Some<number>["getOrCompute"]>>().equals<number>();

// Some.tap should return Maybe<T>
check<ReturnType<Some<number>["tap"]>>().equals<Maybe<number>>();

// Some.toResult should return Result<T, Error<unknown>>
check<ReturnType<Some<number>["toResult"]>>().equals<Result<number, Error<unknown>>>();

// =============================================================================
// None Type
// =============================================================================

// None should have ok: false
check<None["ok"]>().equals<false>();

// None.isSome() should return false
check<ReturnType<None["isSome"]>>().equals<false>();

// None.isNone() should return true
check<ReturnType<None["isNone"]>>().equals<true>();

// None.equals uses Maybe<unknown> - deliberate design choice since None has no value type T
check<None["equals"]>().equals<(other: Maybe<unknown>) => boolean>();

// None.equals with comparator uses Maybe<unknown>
type NoneEqualsWithComparatorFn = (other: Maybe<unknown>, comparator: (a: unknown, b: unknown) => boolean) => boolean;
check<None["equals"]>().equals<NoneEqualsWithComparatorFn>();

// None.filter should return None
check<ReturnType<None["filter"]>>().equals<None>();

// None.map should return None (never input)
check<ReturnType<None["map"]>>().equals<None>();

// None.flatMap should return None (never input)
check<ReturnType<None["flatMap"]>>().equals<None>();

// None.getOrElse should return T (takes default)
check<None["getOrElse"]>().equals<<T>(defaultValue: T) => T>();

// None.getOrCompute should return T (computes default)
check<None["getOrCompute"]>().equals<<T>(_fn: () => T) => T>();

// None.tap should return None (never input)
check<ReturnType<None["tap"]>>().equals<None>();

// None.toResult should return Result<never, Error<unknown>>
check<ReturnType<None["toResult"]>>().equals<Result<never, Error<unknown>>>();
Loading
Loading