Complex typing of a factory function returning a wrapper around a function in Typescript (generics of generics) - Stack Overflow

I want to create a factory function that takes an onOk and onErr handlers and return a wrapper around f

I want to create a factory function that takes an onOk and onErr handlers and return a wrapper around function that may throw.

const mayThrow = (fail: boolean) => {
   if (fail) {
      throw new Error('failed')
   }
   return 'ok'
}

Let's define both handler and their types

type Ok<T> = { ok: true, value: T }
type Err<E> = { ok: false, error: E }
type Result<T, E> = Ok<T> | Err<E>
const onOk = <T>(value: T): Ok<T> => ({ ok: true, value })
const onErr = <E>(error: E): Err<E> => ({ ok: false, error })

I have this factory function that I can almost type correctly.

function createSafeFactory<
   U extends (...args: any[]) => any,
   F extends (...args: any[]) => any
>(onOk: U, onErr: F) {
   return <A extends any[], T, E>(
      fn: (...args: A) => T,
      errorFn?: (error: unknown) => E
   ) => {
       return (...args: A): ReturnType<U> | ReturnType<F> => {
           try {
               const result = fn(...args)
               return onOk(result)
           } catch (e) {
               const error = errorFn ? errorFn(e) : (e as E)
               return onErr(error)
           }
       }
   }
}

const safe = createSafeFactory(onOk, onErr)
// const safe: <A extends any[], T, E>(fn: (...args: A) => T, errorFn?: ((error: unknown) => E) | undefined) => (...args: A) => Ok<unknown> | Err<unknown>

const safeFn = safe(mayThrow)
// const safeFn: (fail: boolean) => Ok<unknown> | Err<unknown>

const res = safeFn(true)
// const res: Ok<unknown> | Err<unknown>

if (res.ok) {
   const data = res.value
   // const data: unknown
}

data is unknown instead of string

What I want is the result to be typed Result<string, never> or Ok<string> | Err<never>.

The problem is here: ReturnType<U> | ReturnType<F>. I want the return type of U and F given that U and F are typed relative to T and E.

How can I achieve something like ReturnType<U<T>> | ReturnType<F<E>>

I want to create a factory function that takes an onOk and onErr handlers and return a wrapper around function that may throw.

const mayThrow = (fail: boolean) => {
   if (fail) {
      throw new Error('failed')
   }
   return 'ok'
}

Let's define both handler and their types

type Ok<T> = { ok: true, value: T }
type Err<E> = { ok: false, error: E }
type Result<T, E> = Ok<T> | Err<E>
const onOk = <T>(value: T): Ok<T> => ({ ok: true, value })
const onErr = <E>(error: E): Err<E> => ({ ok: false, error })

I have this factory function that I can almost type correctly.

function createSafeFactory<
   U extends (...args: any[]) => any,
   F extends (...args: any[]) => any
>(onOk: U, onErr: F) {
   return <A extends any[], T, E>(
      fn: (...args: A) => T,
      errorFn?: (error: unknown) => E
   ) => {
       return (...args: A): ReturnType<U> | ReturnType<F> => {
           try {
               const result = fn(...args)
               return onOk(result)
           } catch (e) {
               const error = errorFn ? errorFn(e) : (e as E)
               return onErr(error)
           }
       }
   }
}

const safe = createSafeFactory(onOk, onErr)
// const safe: <A extends any[], T, E>(fn: (...args: A) => T, errorFn?: ((error: unknown) => E) | undefined) => (...args: A) => Ok<unknown> | Err<unknown>

const safeFn = safe(mayThrow)
// const safeFn: (fail: boolean) => Ok<unknown> | Err<unknown>

const res = safeFn(true)
// const res: Ok<unknown> | Err<unknown>

if (res.ok) {
   const data = res.value
   // const data: unknown
}

data is unknown instead of string

What I want is the result to be typed Result<string, never> or Ok<string> | Err<never>.

The problem is here: ReturnType<U> | ReturnType<F>. I want the return type of U and F given that U and F are typed relative to T and E.

How can I achieve something like ReturnType<U<T>> | ReturnType<F<E>>

Share Improve this question asked Nov 15, 2024 at 21:21 eakleakl 5711 gold badge10 silver badges24 bronze badges 4
  • TS cannot support arbitrary high order function types like this. The only support that exists is described at ms/TS#30215. The closest you can get to this exact code is this playground link but unfortunately the returned function can only accept any[], it cannot already have been generic. If you want that you have to give up on the currying thing and write it as in this playground link. Does that fully address the q? If so I'll write an a; if not, what's missing? – jcalz Commented Nov 15, 2024 at 22:07
  • Thanks @jcalz that’s helpful. It is possible to define the final return type explicitly while letting all other types to be inferred by TS? ˋcreateSafeFactory<R, U, F>(…) { … }ˋ and when invoking the function ˋcreateSafeFacrory<Ok<T>|Err<E>>(…)ˋ with T, the infered return type of onOk and E the infered return type of onErr – eakl Commented Nov 16, 2024 at 4:46
  • No, you cannot explicitly annotate them; the support in ms/TS#30215 does stuff that simply cannot be expressed in the type system. The types you'd want to annotate would involve unbound type parameters. Have I fully addressed the question now? Shall I write up an answer or am I missing something? (Note I asked this question before but you didn't answer it, could you please do so, so I can know how to proceed? Thank you.) – jcalz Commented Nov 16, 2024 at 15:53
  • Yes, you can, thanks for the explanation. – eakl Commented Nov 16, 2024 at 17:55
Add a comment  | 

1 Answer 1

Reset to default 0

You cannot perform arbitrary higher order generic function type manipulation. Generally speaking TypeScript will have to give up and erase generic type parameters or instantiate them with their constraints before manipulating them. So ReturnType<> acting on the type <T>(value: T) => Ok<T> will produce Ok<unknown> because the generic type parameter T inside the function ends up being instantiated with its implicit unknown constraint. TypeScript's type system is simply not expressive enough for this. In order to allow such things you might need higher kinded types as described in microsoft/TypeScript#1213, or instantiation types as requested in microsoft/TypeScript#50481.

There is some support for manipulating generic function types like this at the value level. That is, sometimes you can write generic functions that operate on generic functions and produce different generic functions, but you need actual functions to do this, and not just their types. This support was implemented in microsoft/TypeScript#30215, and it's quite restrictive in what it supports. In particular:

When an argument expression in a function call is of a generic function type, the type parameters of that function type are propagated onto the result type of the call if:

  • the called function is a generic function that returns a function type with a single call signature,
  • that single call signature doesn't itself introduce type parameters, and
  • in the left-to-right processing of the function call arguments, no inferences have been made for any of the type parameters referenced in the contextual type for the argument expression.

Generally speaking you cannot write things in terms of function types like O extends (arg: any) => any and E extends (arg: any) => any, but instead need to write things in terms of their argument and return types like OA, OR, EA, and ER. Unfortunately that isn't sufficient for your code to start working:

function createSafeFactory<OA, OR, EA, ER>(
    onOk: (arg: OA) => OR,
    onErr: (arg: EA) => ER
) {
    return <A extends any[]>(
        fn: (...args: A) => OA,
        errorFn?: (error: unknown) => EA
    ) => {
        return (...args: A): OR | ER => {
            try {
                const result = fn(...args)
                return onOk(result)
            } catch (e) {
                const error = errorFn ? errorFn(e) : e as EA //

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745670918a4639397.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信