I am working on a codebase with lots of asynchronous api involving success
options in their params, like
declare function foo(_: {
success?: (_: string) => void,
fail?: () => void,
}): void
declare function bar(_: {
success?: (_: string) => void,
fail?: () => void,
src?: string
}): void
declare function baz(_: {
success?: (_: string) => void,
fail?: () => void,
expired: number
}): void
declare function foobar(_: {
success?: (_: string) => void,
fail?: () => void,
token: string,
state: boolean
}): void
i want to promisefy all of them with below code
interface Cont<R> {
fail?: () => void
success?: (_: R) => void
}
interface Suspendable<O> {
(option: O): void
}
function suspend<R, O extends Cont<R>>(fn: Suspendable<O>) {
return async (opt: Omit<Omit<O, "success">, "fail">) => await new Promise<R>((resolve, _) => fn({
...opt,
success: it => resolve(it),
fail: ()=> resolve(undefined)
} as O )) // if any chance, I'd like to omit the `as O` but forgive it for now
}
(async () => {
let _foo = await suspend(foo)({}) // good
let _bar = await suspend(bar)({}) // good
let _baz = await suspend(baz)/* compile error here */ ({ expired: 100})
})()
did I missed some goodies in typescript to help me capture the real type of the O
in the fn
param so that I can constraint the params nicely and pass the compiler error ?
There's two ways I can think of to go here. The first is to give up on having two type parameters, and just infer O
and compute R
from it:
function suspend<O extends Cont<any>>(fn: Suspendable<O>) {
return async (opt: Omit<O, "success" | "fail">) =>
await new Promise<Parameters<Exclude<O["success"], undefined>>[0]>(
(resolve, _) => fn({
...opt,
success: it => resolve(it),
fail: () => resolve(undefined)
} as O));
}
The computation for R
is the unfortunate-looking Parameters<Exclude<O["success"], undefined>>[0]
. Now the following compiles with no error:
(async () => {
let _foo = await suspend(foo)({})
let _bar = await suspend(bar)({})
let _baz = await suspend(baz)({ expired: 2 })
})()
The error about O
you get if you don't assert as O
is a good one; it's possible that fn()
's argument has success
or fail
properties that are narrower than those specified in Cont<any>
:
declare function hmm(opt: {
success?: (_: unknown) => void,
fail?: () => string // narrower than specified in Cont<any>
}): void;
suspend(hmm);
And since you are calling fn()
with an arg having hand-rolled success
and fail
methods, you can't guarantee that it will behave as fn()
expects. So I think you'll need to assert and move on.
The other way you can go is to keep two type parameters, and rely on the fact that success
and fail
are optional methods to make it work:
function suspend2<O, R>(fn: Suspendable<O & Cont<R>>) {
return async (opt: O) =>
await new Promise<R>(
(resolve, _) => fn({
...opt,
success: (it: R) => resolve(it),
fail: () => resolve(undefined)
}));
}
Here we are inferring both `O` and `R` from an argument `fn` of type `Suspendable<O & Cont<R>>`. Ideally this would automatically yield the right `R` as well as an `O` object type that doesn't include `success` or `fail` methods. But what happens is that you'll get the right `R` (yay) but the value of `O` will be the full object type including the `success` and `fail` methods (boo).
At least now though you don't need type assertions like as O
, since you are passing in a value of type O & Cont<R>
.
The following still works as desired:
(async () => {
let _foo = await suspend2(foo)({})
let _bar = await suspend2(bar)({})
let _baz = await suspend2(baz)({ expired: 2 })
})()
but only because O
doesn't require fail
or success
properties.
Anyway, hope one of those helps you; good luck!