From b640159f18de47dffc36278e69f3e8bc44127835 Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Mon, 18 Nov 2024 22:58:24 -0300 Subject: [PATCH] fix: improve result nullish type #194 --- CHANGELOG.md | 18 ++ changes.txt | 302 ++++++++++++++++++++++++++++++++ lib/core/fail.ts | 10 +- lib/core/result.ts | 31 +++- lib/types.ts | 3 +- package.json | 2 +- tests/core/adapter.spec.ts | 2 +- tests/core/aggregate.spec.ts | 10 +- tests/core/entity.spec.ts | 11 +- tests/core/fail.spec.ts | 13 +- tests/core/value-object.spec.ts | 2 +- 11 files changed, 371 insertions(+), 33 deletions(-) create mode 100644 changes.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 92e902a..ea97199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ All notable changes to this project will be documented in this file. --- +### [1.23.5] - 2024-10-18 + +#### Fix + +## Changelog for `1.23.5-beta.0` + +### **Changes** +- **Explicit Typing for Failures**: `Result.fail` now explicitly returns `Result`, ensuring that values are always `null` in failure states. +- **New `isNull` Method**: Added to simplify validation of `null` values or failure states, improving readability and type safety. +- **Adjusted Creation Methods**: Methods like `create` and adapters now return `Result` where applicable for better consistency and error handling. + +### **Impact** +These changes improve type safety, make failure handling more explicit, and encourage clearer checks in code. The updates may require minor adjustments in existing codebases to accommodate the explicit `null` typing in failures. This release is marked as beta for testing purposes. + +Feedback is welcome! 🚀 + +[issue](https://github.com/4lessandrodev/rich-domain/issues/194) + ## Released --- diff --git a/changes.txt b/changes.txt new file mode 100644 index 0000000..4236913 --- /dev/null +++ b/changes.txt @@ -0,0 +1,302 @@ +diff --git a/lib/core/fail.ts b/lib/core/fail.ts +index 8f0739d..239c95e 100644 +--- a/lib/core/fail.ts ++++ b/lib/core/fail.ts +@@ -16,7 +16,7 @@ import Result from "./result"; + * @argument P generic type for payload. + * @default void as no state. + */ +- function Fail(): Result; ++ function Fail(): Result; + + /** + * @description Create an instance of Result as failure state. +@@ -33,7 +33,7 @@ import Result from "./result"; + * @argument P generic type for payload. + * @default void as no state. + */ +-function Fail(): IResult; ++function Fail(): IResult; + + /** + * @description Create an instance of Result as failure state. +@@ -50,7 +50,7 @@ function Fail(): IResult; + * @argument P generic type for payload. + * @default void as no state. + */ +- function Fail(error: E extends void ? null : E, metaData?: M): Result; ++ function Fail(error: E extends void ? null : E, metaData?: M): Result; + + + /** +@@ -68,7 +68,7 @@ function Fail(): IResult; + * @argument P generic type for payload. + * @default void as no state. + */ +-function Fail(error: E extends void ? null : E, metaData?: M): IResult; ++function Fail(error: E extends void ? null : E, metaData?: M): IResult; + + /** + * @description Create an instance of Result as failure state. +@@ -85,7 +85,7 @@ function Fail(error: E extends void ? null : E, + * @argument P generic type for payload. + * @default void as no state. + */ +-function Fail(error?: E extends void ? null : E, metaData?: M): IResult { ++function Fail(error?: E extends void ? null : E, metaData?: M): IResult { + const _error = (typeof error !== 'undefined' && error !== null) ? error : 'void error. no message!'; + return Result.fail(_error as any, metaData); + } +diff --git a/lib/core/result.ts b/lib/core/result.ts +index 0e26f83..f65011d 100644 +--- a/lib/core/result.ts ++++ b/lib/core/result.ts +@@ -29,7 +29,7 @@ export class Result implements IResult { + * @returns instance of Result. + */ + public static Ok(): Result; +- ++ + /** + * @description Create an instance of Result as success state. + * @returns instance of Result. +@@ -42,7 +42,7 @@ export class Result implements IResult { + * @param metaData as M to state. + * @returns instance of Result. + */ +- public static Ok(data: T, metaData?: M): Result; ++ public static Ok(data: T, metaData?: M): Result; + + /** + * @description Create an instance of Result as success state with data and metadata to payload. +@@ -51,7 +51,7 @@ export class Result implements IResult { + * @returns instance of Result. + */ + public static Ok(data: T, metaData?: M): IResult; +- ++ + /** + * @description Create an instance of Result as success state with data and metadata to payload. + * @param data as T to payload. +@@ -70,7 +70,7 @@ export class Result implements IResult { + * @param metaData as M to state. + * @returns instance of Result. + */ +- public static fail(error?: D, metaData?: M): Result; ++ public static fail(error?: D, metaData?: M): Result; + + /** + * @description Create an instance of Result as failure state with error and metadata to payload. +@@ -100,7 +100,7 @@ export class Result implements IResult { + */ + public static combine(results: Array>): IResult { + const iterator = Result.iterate(results); +- if (iterator.isEmpty()) return Result.fail('No results provided on combine param' as B) as IResult; ++ if (iterator.isEmpty()) return Result.fail('No results provided on combine param' as B) as unknown as IResult; + while (iterator.hasNext()) { + const currentResult = iterator.next(); + if (currentResult.isFail()) return currentResult as IResult; +@@ -167,6 +167,27 @@ export class Result implements IResult { + isFail(): boolean { + return this.#isFail; + } ++ /** ++ * @description Determines if the result instance contains a `null` value. ++ * This method is particularly useful when dealing with dynamically typed results, ++ * allowing developers to validate and refine types based on the state of the result. ++ * ++ * @returns {boolean} ++ * - `true` if the result instance holds a `null` value. ++ * - `false` otherwise. ++ * ++ * @example ++ * const result = Result.Ok(null); ++ * ++ * if (result.isNull()) { ++ * console.log("The result value is null"); ++ * } else { ++ * console.log("The result value is not null:", result.value()); ++ * } ++ */ ++ isNull(): boolean { ++ return this.#data === null || this.#isFail; ++ } + + /** + * @description Check if result instance is success. +diff --git a/lib/types.ts b/lib/types.ts +index e3af3c0..59989ba 100644 +--- a/lib/types.ts ++++ b/lib/types.ts +@@ -31,6 +31,7 @@ export interface IResult { + value(): T; + error(): D; + isFail(): boolean; ++ isNull(): boolean; + isOk(): boolean; + metaData(): M; + toObject(): IResultObject; +@@ -196,7 +197,7 @@ export interface IPublicHistory { + export type IPropsValidation = { [P in keyof Required]: (value: T[P]) => boolean }; + + export interface IAdapter { +- build(target: F): IResult; ++ build(target: F): IResult; + } + + export interface Adapter { +diff --git a/tests/core/adapter.spec.ts b/tests/core/adapter.spec.ts +index 1fc9f5d..4d9e470 100644 +--- a/tests/core/adapter.spec.ts ++++ b/tests/core/adapter.spec.ts +@@ -104,7 +104,7 @@ describe('adapter v1', () => { + type Err = { err: string; stack?: string }; + + class CustomAdapter implements IAdapter { +- build(target: In): IResult { ++ build(target: In): IResult { + if (typeof target.a !== 'number') return Fail({ err: 'target.a is not a number' }); + return Ok({ b: target.a.toString() }); + } +diff --git a/tests/core/aggregate.spec.ts b/tests/core/aggregate.spec.ts +index 4fffe18..591f627 100644 +--- a/tests/core/aggregate.spec.ts ++++ b/tests/core/aggregate.spec.ts +@@ -114,7 +114,7 @@ describe('aggregate', () => { + return this.validator.number(value).isBetween(0, 130); + } + +- public static create(props: Props): IResult> { ++ public static create(props: Props): IResult | null> { + if (!this.isValidValue(props.value)) return Result.fail('Invalid value'); + return Result.Ok(new AgeVo(props)); + } +@@ -142,14 +142,14 @@ describe('aggregate', () => { + super(props); + } + +- public static create(props: AggProps): IResult> { ++ public static create(props: AggProps): IResult | null> { + return Result.Ok(new UserAgg(props)); + } + } + + it('should create a user with success', () => { + +- const age = AgeVo.create({ value: 21 }).value(); ++ const age = AgeVo.create({ value: 21 }).value() as AgeVo; + const user = UserAgg.create({ age }); + + expect(user.isOk()).toBeTruthy(); +@@ -158,10 +158,10 @@ describe('aggregate', () => { + + it('should get value from age with success', () => { + +- const age = AgeVo.create({ value: 21 }).value(); ++ const age = AgeVo.create({ value: 21 }).value() as AgeVo; + const user = UserAgg.create({ age }).value(); + +- const result = user ++ const result = (user as Aggregate) + .get('age') + .get('value'); + +diff --git a/tests/core/entity.spec.ts b/tests/core/entity.spec.ts +index 2b49d7b..7b34e15 100644 +--- a/tests/core/entity.spec.ts ++++ b/tests/core/entity.spec.ts +@@ -1,4 +1,4 @@ +-import { Entity, Id, Ok, Result, ValueObject } from "../../lib/core"; ++import { Entity, Fail, Id, Ok, Result, ValueObject } from "../../lib/core"; + import { Adapter, IResult, UID } from "../../lib/types"; + + describe("entity", () => { +@@ -16,7 +16,8 @@ describe("entity", () => { + return value !== undefined; + } + +- public static create(props: Props): IResult { ++ public static create(props: Props): IResult { ++ if(!props) return Fail('props is required') + return Result.Ok(new EntitySample(props)) + } + } +@@ -24,7 +25,7 @@ describe("entity", () => { + it('should get prototype', () => { + const ent = EntitySample.create({ foo: 'bar' }); + +- ent.value().change('foo', 'changed'); ++ ent.value()?.change('foo', 'changed'); + expect(ent.isOk()).toBeTruthy(); + }); + }); +@@ -630,7 +631,7 @@ describe("entity", () => { + return this.util.string(this.props.foo).removeSpaces(); + } + +- public static create(props: Props): IResult { ++ public static create(props: Props): IResult { + const isValid = this.isValidProps(props.foo); + if (!isValid) return Result.fail('Erro'); + return Result.Ok(new ValSamp(props)) +@@ -645,7 +646,7 @@ describe("entity", () => { + it('should remove space from value', () => { + const ent = ValSamp.create({ foo: ' Some Value With Spaces ' }); + expect(ent.isOk()).toBeTruthy(); +- expect(ent.value().RemoveSpace()).toBe('SomeValueWithSpaces'); ++ expect(ent.value()?.RemoveSpace()).toBe('SomeValueWithSpaces'); + }); + }); + +diff --git a/tests/core/fail.spec.ts b/tests/core/fail.spec.ts +index 97f79f3..9adf361 100644 +--- a/tests/core/fail.spec.ts ++++ b/tests/core/fail.spec.ts +@@ -97,11 +97,7 @@ describe('fail', () => { + arg: string; + } + +- interface Payload { +- user: any; +- } +- +- const result = Fail({ message: 'invalid email' }, { arg: 'invalid@mail.com' }); ++ const result = Fail({ message: 'invalid email' }, { arg: 'invalid@mail.com' }); + expect(result.isOk()).toBeFalsy(); + expect(result.isFail()).toBeTruthy(); + expect(result.toObject()).toEqual({ +@@ -116,7 +112,6 @@ describe('fail', () => { + describe('generic types', () => { + + type Error = { message: string }; +- type Payload = { data: { status: number } }; + type MetaData = { args: number }; + + it('should fail generate the same payload as result', () => { +@@ -125,10 +120,10 @@ describe('fail', () => { + const metaData: MetaData = { args: status }; + const error: Error = { message: 'something went wrong!' }; + +- const resultInstance = Result.fail(error, metaData); +- const okInstance = Fail(error, metaData); ++ const resultInstance = Result.fail(error, metaData); ++ const failInstance = Fail(error, metaData); + +- expect(resultInstance.toObject()).toEqual(okInstance.toObject()); ++ expect(resultInstance.toObject()).toEqual(failInstance.toObject()); + + }); + +diff --git a/tests/core/value-object.spec.ts b/tests/core/value-object.spec.ts +index 1add011..5530694 100644 +--- a/tests/core/value-object.spec.ts ++++ b/tests/core/value-object.spec.ts +@@ -328,7 +328,7 @@ describe('value-object', () => { + return isValidAge && isValidDate; + } + +- public static create(props: Props1): IResult { ++ public static create(props: Props1): IResult { + if (!HumanAge.isValidProps(props)) return Result.fail('Invalid props'); + return Result.Ok(new HumanAge(props)); + } diff --git a/lib/core/fail.ts b/lib/core/fail.ts index 8f0739d..239c95e 100644 --- a/lib/core/fail.ts +++ b/lib/core/fail.ts @@ -16,7 +16,7 @@ import Result from "./result"; * @argument P generic type for payload. * @default void as no state. */ - function Fail(): Result; + function Fail(): Result; /** * @description Create an instance of Result as failure state. @@ -33,7 +33,7 @@ import Result from "./result"; * @argument P generic type for payload. * @default void as no state. */ -function Fail(): IResult; +function Fail(): IResult; /** * @description Create an instance of Result as failure state. @@ -50,7 +50,7 @@ function Fail(): IResult; * @argument P generic type for payload. * @default void as no state. */ - function Fail(error: E extends void ? null : E, metaData?: M): Result; + function Fail(error: E extends void ? null : E, metaData?: M): Result; /** @@ -68,7 +68,7 @@ function Fail(): IResult; * @argument P generic type for payload. * @default void as no state. */ -function Fail(error: E extends void ? null : E, metaData?: M): IResult; +function Fail(error: E extends void ? null : E, metaData?: M): IResult; /** * @description Create an instance of Result as failure state. @@ -85,7 +85,7 @@ function Fail(error: E extends void ? null : E, * @argument P generic type for payload. * @default void as no state. */ -function Fail(error?: E extends void ? null : E, metaData?: M): IResult { +function Fail(error?: E extends void ? null : E, metaData?: M): IResult { const _error = (typeof error !== 'undefined' && error !== null) ? error : 'void error. no message!'; return Result.fail(_error as any, metaData); } diff --git a/lib/core/result.ts b/lib/core/result.ts index 0e26f83..f65011d 100644 --- a/lib/core/result.ts +++ b/lib/core/result.ts @@ -29,7 +29,7 @@ export class Result implements IResult { * @returns instance of Result. */ public static Ok(): Result; - + /** * @description Create an instance of Result as success state. * @returns instance of Result. @@ -42,7 +42,7 @@ export class Result implements IResult { * @param metaData as M to state. * @returns instance of Result. */ - public static Ok(data: T, metaData?: M): Result; + public static Ok(data: T, metaData?: M): Result; /** * @description Create an instance of Result as success state with data and metadata to payload. @@ -51,7 +51,7 @@ export class Result implements IResult { * @returns instance of Result. */ public static Ok(data: T, metaData?: M): IResult; - + /** * @description Create an instance of Result as success state with data and metadata to payload. * @param data as T to payload. @@ -70,7 +70,7 @@ export class Result implements IResult { * @param metaData as M to state. * @returns instance of Result. */ - public static fail(error?: D, metaData?: M): Result; + public static fail(error?: D, metaData?: M): Result; /** * @description Create an instance of Result as failure state with error and metadata to payload. @@ -100,7 +100,7 @@ export class Result implements IResult { */ public static combine(results: Array>): IResult { const iterator = Result.iterate(results); - if (iterator.isEmpty()) return Result.fail('No results provided on combine param' as B) as IResult; + if (iterator.isEmpty()) return Result.fail('No results provided on combine param' as B) as unknown as IResult; while (iterator.hasNext()) { const currentResult = iterator.next(); if (currentResult.isFail()) return currentResult as IResult; @@ -167,6 +167,27 @@ export class Result implements IResult { isFail(): boolean { return this.#isFail; } + /** + * @description Determines if the result instance contains a `null` value. + * This method is particularly useful when dealing with dynamically typed results, + * allowing developers to validate and refine types based on the state of the result. + * + * @returns {boolean} + * - `true` if the result instance holds a `null` value. + * - `false` otherwise. + * + * @example + * const result = Result.Ok(null); + * + * if (result.isNull()) { + * console.log("The result value is null"); + * } else { + * console.log("The result value is not null:", result.value()); + * } + */ + isNull(): boolean { + return this.#data === null || this.#isFail; + } /** * @description Check if result instance is success. diff --git a/lib/types.ts b/lib/types.ts index e3af3c0..59989ba 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -31,6 +31,7 @@ export interface IResult { value(): T; error(): D; isFail(): boolean; + isNull(): boolean; isOk(): boolean; metaData(): M; toObject(): IResultObject; @@ -196,7 +197,7 @@ export interface IPublicHistory { export type IPropsValidation = { [P in keyof Required]: (value: T[P]) => boolean }; export interface IAdapter { - build(target: F): IResult; + build(target: F): IResult; } export interface Adapter { diff --git a/package.json b/package.json index 16fd2bb..6a72135 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rich-domain", - "version": "1.23.4", + "version": "1.23.5-beta-0", "description": "This package provide utils file and interfaces to assistant build a complex application with domain driving design", "main": "index.js", "types": "index.d.ts", diff --git a/tests/core/adapter.spec.ts b/tests/core/adapter.spec.ts index 1fc9f5d..4d9e470 100644 --- a/tests/core/adapter.spec.ts +++ b/tests/core/adapter.spec.ts @@ -104,7 +104,7 @@ describe('adapter v1', () => { type Err = { err: string; stack?: string }; class CustomAdapter implements IAdapter { - build(target: In): IResult { + build(target: In): IResult { if (typeof target.a !== 'number') return Fail({ err: 'target.a is not a number' }); return Ok({ b: target.a.toString() }); } diff --git a/tests/core/aggregate.spec.ts b/tests/core/aggregate.spec.ts index 4fffe18..b54ad2a 100644 --- a/tests/core/aggregate.spec.ts +++ b/tests/core/aggregate.spec.ts @@ -114,7 +114,7 @@ describe('aggregate', () => { return this.validator.number(value).isBetween(0, 130); } - public static create(props: Props): IResult> { + public static create(props: Props): IResult | null> { if (!this.isValidValue(props.value)) return Result.fail('Invalid value'); return Result.Ok(new AgeVo(props)); } @@ -142,14 +142,14 @@ describe('aggregate', () => { super(props); } - public static create(props: AggProps): IResult> { + public static create(props: AggProps): Result | null> { return Result.Ok(new UserAgg(props)); } } it('should create a user with success', () => { - const age = AgeVo.create({ value: 21 }).value(); + const age = AgeVo.create({ value: 21 }).value() as AgeVo; const user = UserAgg.create({ age }); expect(user.isOk()).toBeTruthy(); @@ -158,10 +158,10 @@ describe('aggregate', () => { it('should get value from age with success', () => { - const age = AgeVo.create({ value: 21 }).value(); + const age = AgeVo.create({ value: 21 }).value() as AgeVo; const user = UserAgg.create({ age }).value(); - const result = user + const result = (user as Aggregate) .get('age') .get('value'); diff --git a/tests/core/entity.spec.ts b/tests/core/entity.spec.ts index 2b49d7b..7b34e15 100644 --- a/tests/core/entity.spec.ts +++ b/tests/core/entity.spec.ts @@ -1,4 +1,4 @@ -import { Entity, Id, Ok, Result, ValueObject } from "../../lib/core"; +import { Entity, Fail, Id, Ok, Result, ValueObject } from "../../lib/core"; import { Adapter, IResult, UID } from "../../lib/types"; describe("entity", () => { @@ -16,7 +16,8 @@ describe("entity", () => { return value !== undefined; } - public static create(props: Props): IResult { + public static create(props: Props): IResult { + if(!props) return Fail('props is required') return Result.Ok(new EntitySample(props)) } } @@ -24,7 +25,7 @@ describe("entity", () => { it('should get prototype', () => { const ent = EntitySample.create({ foo: 'bar' }); - ent.value().change('foo', 'changed'); + ent.value()?.change('foo', 'changed'); expect(ent.isOk()).toBeTruthy(); }); }); @@ -630,7 +631,7 @@ describe("entity", () => { return this.util.string(this.props.foo).removeSpaces(); } - public static create(props: Props): IResult { + public static create(props: Props): IResult { const isValid = this.isValidProps(props.foo); if (!isValid) return Result.fail('Erro'); return Result.Ok(new ValSamp(props)) @@ -645,7 +646,7 @@ describe("entity", () => { it('should remove space from value', () => { const ent = ValSamp.create({ foo: ' Some Value With Spaces ' }); expect(ent.isOk()).toBeTruthy(); - expect(ent.value().RemoveSpace()).toBe('SomeValueWithSpaces'); + expect(ent.value()?.RemoveSpace()).toBe('SomeValueWithSpaces'); }); }); diff --git a/tests/core/fail.spec.ts b/tests/core/fail.spec.ts index 97f79f3..9adf361 100644 --- a/tests/core/fail.spec.ts +++ b/tests/core/fail.spec.ts @@ -97,11 +97,7 @@ describe('fail', () => { arg: string; } - interface Payload { - user: any; - } - - const result = Fail({ message: 'invalid email' }, { arg: 'invalid@mail.com' }); + const result = Fail({ message: 'invalid email' }, { arg: 'invalid@mail.com' }); expect(result.isOk()).toBeFalsy(); expect(result.isFail()).toBeTruthy(); expect(result.toObject()).toEqual({ @@ -116,7 +112,6 @@ describe('fail', () => { describe('generic types', () => { type Error = { message: string }; - type Payload = { data: { status: number } }; type MetaData = { args: number }; it('should fail generate the same payload as result', () => { @@ -125,10 +120,10 @@ describe('fail', () => { const metaData: MetaData = { args: status }; const error: Error = { message: 'something went wrong!' }; - const resultInstance = Result.fail(error, metaData); - const okInstance = Fail(error, metaData); + const resultInstance = Result.fail(error, metaData); + const failInstance = Fail(error, metaData); - expect(resultInstance.toObject()).toEqual(okInstance.toObject()); + expect(resultInstance.toObject()).toEqual(failInstance.toObject()); }); diff --git a/tests/core/value-object.spec.ts b/tests/core/value-object.spec.ts index 1add011..5530694 100644 --- a/tests/core/value-object.spec.ts +++ b/tests/core/value-object.spec.ts @@ -328,7 +328,7 @@ describe('value-object', () => { return isValidAge && isValidDate; } - public static create(props: Props1): IResult { + public static create(props: Props1): IResult { if (!HumanAge.isValidProps(props)) return Result.fail('Invalid props'); return Result.Ok(new HumanAge(props)); }