The Result type is one of those things that always sounds great in theory but often over complicates trivial problems. For instance, what's the difference between getUser: Result<User, UserNotFound>
and getUser: User | null
?
For error handling, you also want to draw a line between expected errors and unexpected:
Most of the time on #2 type errors, you'll handle them all the same (log the error and return an unexpected error), and these are best done with throwing exceptions
For the #1 type errors, I'd call that more business logic and should just be encoded as it's own type on a case by case basis
declare function getUser(id: string): User | null;
type Withdraw =
| {result: "NOT_ENOUGH_FUNDS"}
| {result: "SUCCESS", amount: number}
declare function withdraw(amt: number): Withdraw;
type Validation<T> =
| {valid: false}
| {valid: true, data: T}
declare function validate<T>(model: T): Validation<T>;