Consider this Result
type:
type Result<TResult, TError> =
| {
success: true;
data: TResult;
}
| {
success: false;
error: TError;
};
The Result
type has two type parameters for TResult
and TError
.
It returns a discriminated union, with one branch indicating success and returning data, and the other pointing to failure and returning an error.
Next we have a createRandomNumber
function that returns a Result
type with a number
as the data and an Error
as the error:
const createRandomNumber = (): Result<number, Error> => {
const num = Math.random();
if (num > 0.5) {
return {
success: true,
data: 123,
};
}
return {
success: false,
error: new Error("Something went wrong"),
};
};
This function generates a random number and based on its value, renders a Result
type. If the number exceeds 0.5, it returns a successful result with some data. Otherwise, it returns a failure result with an error.
When we create a result
variable by calling createRandomNumber
, we can see that it is typed as Result
:
const result = createRandomNumber();
// hovering over result shows:
const result: Result<number, Error>
We in turn can conditionally check result.success
and obtain the correct type for result.data
. For example, if result.success
is true, then result.data
is typed as a number:
const result = createRandomNumber();
if (result.success) {
console.log(result.data);
type test = Expect<Equal<typeof result.data, number>>;
} else {
console.error(result.error);
type test = Expect<Equal<typeof result.error, Error>>;
}
This pattern proves very handy for error handling, as it eliminates the need for try-catch
blocks. Instead, we can directly check if the result was successful and act accordingly, or deal with the error.