#library design: a concise way to delegate `Error` variant of `Result` union while keeping type info

1 messages · Page 1 of 1 (latest)

hasty falcon
#

Currently I'm designing a library, and there is my own type Result<T, E>, which is a union of Ok<T> and Error<E> variants. This type will be used frequently in the codebase so for convenience I'm going to write a utility that delegates the Error<E> variant to outer scope, like ? operator in Rust. At the first I designed the utility like this:

const wrap = <T>(fn: () => T): Result<T, unknown> => {
  try {
    const res = fn()
    return somehowWrapOk(res)
  } catch (err) {
    return somehowWrapError(err)
  }
}
const req/*uire*/: <T>(x: unknown, r: Result<T, any>) => asserts x is T = (x, r) => {
  if (/* the Result is Error */) throw // the error
}
// simplified

declare const x: unknown
const _: Result<number, unknown> = wrap(() => {
  req(x, 0 as any as Result<number, unknown>)
  return x // number
})
```This works, but it loses type information about `E` :\/ Is there a way to keep the type information while keeping conciseness?

\* And I realized this "widening" behavior may help me:
```ts
const arr = [] // arr: any[]
arr.push(0)
arr.push('')
arr // arr: (string | number)[]
```and it actually worked:
```ts
const err = []
err[0] = /*T*/ extractErrorType(someResult1) // someResult1: Result<any, T>
err[0] = /*U*/ extractErrorType(someResult2) // someResult2: Result<any, U>
err // (T | U)[]
```but I feel this is ugly 😕
Maybe I should just discard type information about `E` variant?
desert cipher
#

Is the idea here that inner functions still throw as a way to simplify bubbling, which the outer wrap catches and turns it back into the error as value paradigm, and you want the error type to be preserved?

hasty falcon
#

yes, exactly

desert cipher
#

I'm not aware of a way to do that at least not with try/catch, type information of what can be thrown is just not preserved.

#

On a separate note, wrap handles turning exception back to error as value paradigm, then how would the result of what got wraped to be further consumed by upstream? In your example, const _ is presumably the top level so handling of error has to happen there, but what if it's instead in some other code that wants to continue bubble further up, do they now have to manually check the result type and throw again?

hasty falcon
desert cipher
#

So wrap is actually user land and not used by your own code?

hasty falcon
#

wrap will be used on both side

hasty falcon
desert cipher
#

Ah I think I get what you are trying to do, so insted of writing code like:

const fn = () => {
    const result1 = fn1()
    if (result1.type === 'error') return result1

    const result2 = fn2(result1)
    if (result2.type === 'error') return result2

    return fn3(result2)
}

You would write:

const fn = () => wrap(() => {
    const result1 = req(fn1())

    const result2 = req(fn2(result1))

    return req(fn3(result2))
})
hasty falcon
#

yes, that's the point

desert cipher
#

Interesting idea.

#

Then back to your original question, I think you are just constrained by the fact that TS doesn't preserve type information with throw, so that wouldn't work as your bubbling mechanism.

hasty falcon
#

yes

desert cipher
#

Have you looked into Effect? IIRC they achieve it by using generator.

hasty falcon
#

by yielding errors? interesting

hasty falcon
#

uhh, I.. I tested some code and finally wrote:

const string = function*(x: unknown): Generator<'error', string> {
    if (typeof x != 'string') throw yield 'error'
    return x
}

declare const x: unknown
const res = yield* string(x) // res: string

throw is there... 😔 because yields can be resumed and if it is resumed it will return in any case (by design it will never be actually resumed though)

hasty falcon
#
const reverse = <Args extends Array<unknown>, T, R>(gf: (...args: Args) => Generator<T, R, R>): (...args: Args) => Generator<R, T, T> =>
    function*(...args: Args) {
        const res = gf(...args).next()
        if (res.done) return yield res.value
        else return res.value
    }
const string = reverse(function*(x: unknown): Generator<string, 'err', 'err'> {
    if (typeof x != 'string') return 'err' as const
    return yield x
})
```haha, amazing