Issue
I have typescript code as follows:
function loader<P extends [], T>(load: (...args: P) => Promise<T>, lazy = false) {
const result: { value?: T, error?: unknown } = {}
function start(...args: P) {
load(...args)
.then(v => result.value = v)
.catch(e => result.error = e)
}
// Argument of type '[]' is not assignable to parameter of type 'P'.
if (!lazy) start()
return { restart: start, result }
}
const load = (a = 1, b = 2) => Promise.resolve(a + b)
// restart is () => void
const { restart, result } = loader(load)
I need to limit the parameter load
so that it can be called without parameters, that is, it has no parameters or all parameters are optional. And same to function start
.
Moreover, I hope restart
and load
have the same parameter list. That is, restart
should be (a?: number, b?: number) => Promise<number>
.
Solution
Function types are contravariant in their parameter types, meaning that if X extends Y
then ((a: Y) => void) extends ((a: X) => void)
and not vice versa. This is sometimes surprising to people, but it makes sense when you think about the expectations of the receiver of data. My beagle Snoopy is a dog, but a kennel that only accepts beagles named Snoopy wouldn't really be a "dog kennel". The more freedom function callers have, the less freedom the function implementer has, and vice versa.
So, if you're looking for "any function that accepts no arguments", you are not trying to constrain the parameter list P
such that P extends []
. That constraint is exactly backwards. You really want [] extends P
. That is, you want []
to be a lower bound on P
, not an upper bound. The upper bound on P
is just any[]
.
But unfortunately TypeScript does not natively support lower bound generics. There is a longstanding open issue at microsoft/TypeScript#14520 asking for that. Maybe someday that will be supported, and you can write <P super [] extends any[]>
or maybe <[] extends P extends any[]>
, but for now there's no direct support for it.
You'll have to work around it.
One workaround is to just give P
an upper bound of any[]
and then use conditional types to accept or reject a call depending on whether or not [] extends P
:
function loader<P extends any[], T>(
load: [] extends P ? (...args: P) => Promise<T> : never, lazy = false) {
const result: { value?: T, error?: unknown } = {}
function start(...args: P) {
load(...args)
.then(v => result.value = v)
.catch(e => result.error = e)
}
if (!lazy) (start as () => void)() // need an assertion here
return { restart: start, result }
}
Note that the type assertion start as () => void
is necessary because even though the conditional type should enforce things from the callers' end, the compiler's not smart enough to evaluate it inside the generic function.
Anyway, let's test it:
const load = (a = 1, b = 2) => Promise.resolve(a + b)
const { restart, result } = loader(load); // okay
// const restart: (a?: number, b?: number) => void
const badLoad = (a = 1, b: number) => Promise.resolve(a + b);
loader(badLoad); // error
Looks good. The compiler allows loader(load)
but rejects loader(badLoad)
.
Another possible workaround here is just to infer P
from the input function, but make sure that the function parameter list is assignable to Partial<P>
, using the Partial
utility type to convert the parameter list to one with all optional elements:
function loader<P extends any[], T>(load: (...args: Partial<P>) => Promise<T>, lazy = false) {
// ✂ ⋯ same impl as above ⋯ ✂ //
}
const load = (a = 1, b = 2) => Promise.resolve(a + b)
const { restart, result } = loader(load)
// const restart: (a?: number, b?: number) => void
const badLoad = (a = 1, b: number) => Promise.resolve(a + b);
loader(badLoad); // error
Also looks good. You still can't do much inside the implementation to avoid the type assertion here either. The compiler's just not willing to let you call start()
without explicitly telling it that start
allows that. Maybe if lower bound constraints are ever natively supported, then the compiler will be able to verify the safety of such calls automatically. For now, though, this is the best I can do.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.