Result.value return type does not match the docs.
Is your feature request related to a problem? Please describe.
The docs in the Result class say, that the value method returns null if result is a failure.
But the return type is T and not T | null.
This could lead to bugs where the null case it not handled.
Describe the solution you'd like
I would like the return type to be T | null.
Describe alternatives you've considered
Since this would break a lot of code bases, we could at a valueOrNull function.
Additional context I don't know if this is intentional, but it took me by surprise.
Awesome project ❤️
Thank You
Hi, Animii!
I want to thank you for your feedback regarding the value() method typing in the Result class. Your observation about the inconsistency between the documentation and the actual behavior was good and valid.
I’ve adjusted the typing to explicitly reflect that the value will be null in failure cases and introduced the isNull() method to simplify these checks.
Example with the Resolution
With the changes, here’s an example of how to use the updated implementation:
const result = Result.fail("An error occurred");
// Checking if the value is `null` or if the state is a failure
if (result.isNull()) console.log("The result is null or in a failure state:", result.error());
// Using it in a factory method
class UserEntity {
private constructor(public readonly name: string) {}
// Ensure provide null possibly option type
public static create(name: string): Result<UserEntity | null> {
if (!name) return Result.fail("Name is required");
return Result.Ok(new UserEntity(name));
}
}
const userResult = UserEntity.create("");
// now value is possibly null
console.log("User created:", userResult.value()?.name);
Beta Version for Testing
This resolution will be published in 1.23.5.beta-0 for testing. Any feedback you provide will be greatly appreciated as we continue to enhance the library. Once again, thank you so much for your contribution! 🚀
🚨🚨🚨
I understand the adjustments made to the typing @4lessandrodev , but ultimately, it's not great and I think it’s a major regression because it makes things more complex and we end up having to handle more cases.
This change introduces a huge breaking change in the existing code using your type-ddd library. When we used Result.fail(), we now have to handle a null value systematically, which in most cases (if not all cases) we don't need to manage.
If I take your example, before we simply had to do this:
// Using it in a factory method
class UserEntity {
private constructor(public readonly name: string) {}
// Ensure provide null possibly option type
public static create(name: string): Result<UserEntity | null> {
if (!name) return Result.fail("Name is required");
return Result.Ok(new UserEntity(name));
}
}
const userResult = UserEntity.create("");
if(userResult.isFail()) return Result.fail("An error occurred in creating user ");
console.log("User created:", userResult.value().name); // userResult is typing UserEntity
With your modification, userResult is now UserEntity | null.
Any access to a property of the entity or its use in a method (like saving it in a repository) now has to be typed as UserEntity | null, which makes the usage much harder to manage.
Another example from my context after integrating your change:
In the screenshot, you can see the issue.
emailValueObject.value() is set to null even though I am checking it with if (emailValueObject.isFail()) return Result.fail(emailValueObject.error());
And my repository doesn't account for null, and with your change, I’m forced to handle it.
We lose a lot of coherence and intuitiveness with this change.
I think this needs to be reviewed urgently, and I suggest that, in the meantime, we revert this modification.
Hi @GaetanCottrez,
Thank you for your feedback and for highlighting the challenges caused by the recent changes. To address your concerns, I’ve updated the approach to make the dynamic typing in Result optional. This solution caters to both scenarios:
- Explicit Handling of
null: For cases where developers wantResult<T | null>and need to handlenullexplicitly. - Non-Nullable Values: For cases where
nullhandling is unnecessary, allowingResult<T>to be inferred directly.
This provides flexibility without imposing strict changes that disrupt existing integrations.
How It Works
Developers can now choose whether to include null in the type inference of Result based on their specific use case. Below are examples for both scenarios:
Scenario 1: Handling null Explicitly
This is useful when you want to represent potential failure states directly in the Result type.
type Props = { name: string };
class SampleNullish extends Entity<Props> {
private constructor(props: Props) {
super(props);
}
public static create(props: Props): Result<SampleNullish | null> {
// Explicitly typed to allow null
if (!props.name || props.name.trim() === '') {
return Fail('name is required');
}
return Ok(new SampleNullish(props));
}
}
// Usage
const validProps = { name: 'Valid Name' };
const result = SampleNullish.create(validProps);
// Handle null explicitly
const value = result.value();
expect(value?.get('name')).toBe('Valid Name'); // Safe access with optional chaining
In this example, the Result is explicitly typed as Result<SampleNullish | null>, ensuring that the developer treats the result as nullable and uses optional chaining or explicit checks.
Scenario 2: Non-Nullable Values
When null is not a valid state, you can omit it from the type, simplifying the usage:
type Props = { name: string };
class Sample extends Entity<Props> {
private constructor(props: Props) {
super(props);
}
public static create(props: Props): Result<Sample> {
// No null possible, ensuring non-null values
if (!props.name || props.name.trim() === '') {
return Fail('name is required');
}
return Ok(new Sample(props));
}
}
// Usage
const validProps = { name: 'Valid Name' };
const result = Sample.create(validProps);
expect(result.isOk()).toBe(true);
// Confident non-null access
const value = result.value();
expect(value.get('name')).toBe('Valid Name');
In this scenario, the Result is simply Result<Sample>, removing the need for null checks and simplifying the code.
Benefits of the Updated Approach
- Flexibility: Developers can opt for
Result<T | null>orResult<T>based on their use case. - Backward Compatibility: Existing integrations can continue using
Result<T>without disruption. - Clarity: Encourages better type safety and responsibility for handling potential null values.
- Ease of Use: Simplifies common cases where null checks are unnecessary while maintaining the option for explicit handling.
Closing Thoughts
This update aims to balance type safety and usability by making dynamic typing optional. Let me know if this approach resolves your concerns or if further refinements are needed!
New Contract Added: init Method in Domain
I’ve recently introduced a new method, init, to the Domain value object and also available in Entity and Aggregate. This method offers a simpler and more streamlined approach to instance creation by removing the client’s responsibility of validating whether the instantiation was successful.
What is init?
The init method is designed to always return a valid instance of Domain. If the provided value is invalid, it throws an exception, ensuring that the calling code does not need to handle the result validation directly.
How Does This Help?
-
Simplified Usage:
Developers no longer need to handleResultobjects or check if the creation succeeded. The method guarantees a valid instance or throws an error for invalid input. -
Encouraged Error Handling:
By combining this method withtry/catchin lower layers (e.g., infrastructure), you can centralize error handling and reduce repetitive validation logic in upper layers like application services, use case or domain service.
Recommended Usage
Example using phone number available on @type-ddd/phone
In the Domain Layer
Use the init method to ensure a valid MobilePhone instance during domain-level operations:
const mobilePhone = MobilePhone.init("(11) 91234-5678");
console.log(mobilePhone.toCall()); // "011912345678"
In the Infrastructure Layer
Catch any errors resulting from invalid input in lower layers, allowing domain logic to remain clean and focused:
try {
const mobilePhone = MobilePhone.init("(11) 91234-5678");
repository.save(mobilePhone);
} catch (error) {
console.error("Failed to initialize MobilePhone:", error.message);
// Handle or rethrow error as needed
}
Why Use init Over create?
-
init:- Guarantees a valid instance or throws an error.
- Reduces the burden on the calling code to handle validation.
- Suitable for scenarios where exceptions align with your error-handling strategy.
-
create:- Returns a
Result<MobilePhone | null>, requiring validation checks by the caller. - Best used in cases where explicit result handling is preferred or necessary.
- Returns a
Conclusion
The init method is an excellent addition for developers looking to simplify domain layer operations and enforce clear error propagation. While create remains useful in scenarios requiring explicit validation, I encourage you to adopt init alongside try/catch in infrastructure layers for a more intuitive and maintainable design.
This approach ensures that errors are handled where they are most relevant, leaving upper layers free to focus on business logic.
Hi @GaetanCottrez,
Thank you for your feedback and for highlighting the challenges caused by the recent changes. To address your concerns, I’ve updated the approach to make the dynamic typing in
Resultoptional. This solution caters to both scenarios:
- Explicit Handling of
null: For cases where developers wantResult<T | null>and need to handlenullexplicitly.- Non-Nullable Values: For cases where
nullhandling is unnecessary, allowingResult<T>to be inferred directly.This provides flexibility without imposing strict changes that disrupt existing integrations.
How It Works
Developers can now choose whether to include
nullin the type inference ofResultbased on their specific use case. Below are examples for both scenarios:Scenario 1: Handling
nullExplicitlyThis is useful when you want to represent potential failure states directly in the
Resulttype.type Props = { name: string }; class SampleNullish extends Entity<Props> { private constructor(props: Props) { super(props); } public static create(props: Props): Result<SampleNullish | null> { // Explicitly typed to allow null if (!props.name || props.name.trim() === '') { return Fail('name is required'); } return Ok(new SampleNullish(props)); } } // Usage const validProps = { name: 'Valid Name' }; const result = SampleNullish.create(validProps); // Handle null explicitly const value = result.value(); expect(value?.get('name')).toBe('Valid Name'); // Safe access with optional chainingIn this example, the
Resultis explicitly typed asResult<SampleNullish | null>, ensuring that the developer treats the result as nullable and uses optional chaining or explicit checks.Scenario 2: Non-Nullable Values
When
nullis not a valid state, you can omit it from the type, simplifying the usage:type Props = { name: string }; class Sample extends Entity<Props> { private constructor(props: Props) { super(props); } public static create(props: Props): Result<Sample> { // No null possible, ensuring non-null values if (!props.name || props.name.trim() === '') { return Fail('name is required'); } return Ok(new Sample(props)); } } // Usage const validProps = { name: 'Valid Name' }; const result = Sample.create(validProps); expect(result.isOk()).toBe(true); // Confident non-null access const value = result.value(); expect(value.get('name')).toBe('Valid Name');In this scenario, the
Resultis simplyResult<Sample>, removing the need for null checks and simplifying the code.Benefits of the Updated Approach
- Flexibility: Developers can opt for
Result<T | null>orResult<T>based on their use case.- Backward Compatibility: Existing integrations can continue using
Result<T>without disruption.- Clarity: Encourages better type safety and responsibility for handling potential null values.
- Ease of Use: Simplifies common cases where null checks are unnecessary while maintaining the option for explicit handling.
Closing Thoughts
This update aims to balance type safety and usability by making dynamic typing optional. Let me know if this approach resolves your concerns or if further refinements are needed!
@4lessandrodev Dude! You're very fast and very efficient! A huge thank you once again for your work and your library.
When you have time, could you upgrade the version of rich-domain in types-ddd and publish a new version? Also, remember to add the examples described in this issue to the documentation.
New Contract Added:
initMethod inDomainI’ve recently introduced a new method,
init, to theDomainvalue object and also available inEntityandAggregate. This method offers a simpler and more streamlined approach to instance creation by removing the client’s responsibility of validating whether the instantiation was successful.What is
init?The
initmethod is designed to always return a valid instance ofDomain. If the provided value is invalid, it throws an exception, ensuring that the calling code does not need to handle the result validation directly.How Does This Help?
- Simplified Usage: Developers no longer need to handle
Resultobjects or check if the creation succeeded. The method guarantees a valid instance or throws an error for invalid input.- Encouraged Error Handling: By combining this method with
try/catchin lower layers (e.g., infrastructure), you can centralize error handling and reduce repetitive validation logic in upper layers like application services, use case or domain service.Recommended Usage
Example using phone number available on @type-ddd/phone
In the Domain Layer
Use the
initmethod to ensure a validMobilePhoneinstance during domain-level operations:const mobilePhone = MobilePhone.init("(11) 91234-5678"); console.log(mobilePhone.toCall()); // "011912345678"In the Infrastructure Layer
Catch any errors resulting from invalid input in lower layers, allowing domain logic to remain clean and focused:
try { const mobilePhone = MobilePhone.init("(11) 91234-5678"); repository.save(mobilePhone); } catch (error) { console.error("Failed to initialize MobilePhone:", error.message); // Handle or rethrow error as needed }Why Use
initOvercreate?
init:
- Guarantees a valid instance or throws an error.
- Reduces the burden on the calling code to handle validation.
- Suitable for scenarios where exceptions align with your error-handling strategy.
create:
- Returns a
Result<MobilePhone | null>, requiring validation checks by the caller.- Best used in cases where explicit result handling is preferred or necessary.
Conclusion
The
initmethod is an excellent addition for developers looking to simplify domain layer operations and enforce clear error propagation. Whilecreateremains useful in scenarios requiring explicit validation, I encourage you to adoptinitalongsidetry/catchin infrastructure layers for a more intuitive and maintainable design.This approach ensures that errors are handled where they are most relevant, leaving upper layers free to focus on business logic.
I’ll try to use this new approach when I get the chance @4lessandrodev.
However, the try/catch you mention for the infrastructure, isn’t that more for the application layer?
Because normally (and in your example applications in other repositories), the domain is implemented by the application, which in turn is implemented by the infrastructure.
So the infrastructure doesn't have knowledge of the domain, only the application, and therefore, it should be the one using init() (I’m using type-ddd in a hexagonal architecture).