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>>
1 Answer
Reset to default 0You 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 //
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