Reputation: 3705
I'm trying to write a function that accepts a function, and variables that will be passed to the function to fetch more objects for pagination. Meaning that the function's argument must accept skip
, and limit
, but can accept additional variables as well.
See the code to see what I mean:
type PaginationVariables = {
skip: number;
limit: number;
};
type Props<T, EV, V extends EV & PaginationVariables> = {
extraVariables: EV;
fetchMore: (variables: V) => Promise<T[]>;
}
function fn<T, EV, V extends EV & PaginationVariables>({ extraVariables, fetchMore }: Props<T, EV, V>) {
const skip = 0;
const limit = 20
const handleClick = () => {
fetchMore({ skip, limit, ...extraVariables }).then(() => {})
}
}
But it throws:
Argument of type '{ skip: number; limit: number; } & EV' is not assignable to parameter of type 'V'.
'{ skip: number; limit: number; } & EV' is assignable to the constraint of type 'V', but 'V' could be instantiated with a different subtype of constraint 'PaginationVariables'.
I've met with this error before, but I don't know what I'm doing wrong now.
See playground
Upvotes: 1
Views: 101
Reputation: 330376
For a generic function, the caller gets to specify the type parameters. So in your version of fn()
, nothing stops a caller from doing something like this:
fn({
extraVariables: { a: "hello" },
fetchMore(variables: { a: string, skip: number, limit: number, somethingElse: number[] }) {
return Promise.resolve(variables.somethingElse.map(x => String(x)));
}
});
/* fn<
string,
{ a: string; },
{ a: string; skip: number; limit: number; somethingElse: number[]; }
>}(...) */
Here T
is inferred as string
, and EV
is inferred as {a: string}
. So far, so good. But V
is inferred as { a: string; skip: number; limit: number; somethingElse: number[]; }
because the fetchMore()
method expects this type as its variables
input. And since the type of V
is a subtype of EV & PaginationVariables
, the call is completely valid.
But that means, inside the implementation of fn()
, a call like
fetchMore({ skip, limit, ...extraVariables })
is unsafe. In this case it'll end up calling the fetchMore
method with only skip
, limit
, and a
, and not somethingElse
. And so at runtime variables.somethingElse
will be undefined
and not number[]
, and undefined
has no map()
method. Oops, that's a runtime error!
// 💥 TypeError: variables.somethingElse is undefined 💥
That's a problem. You do not want the fetchMore
method to accept some arbitrary subtype of EV & PaginationVariables
selected by the caller. You want it to accept only EV & PaginationVariables
exactly. That is, you don't want anything to be generic in V
at all. If you replace V
with EV & PaginationVariables
, everything starts to work the way you want it to:
type Props<T, EV> = {
extraVariables: EV;
fetchMore: (variables: EV & PaginationVariables) => Promise<T[]>;
}
function fn<T, EV>({ extraVariables, fetchMore }: Props<T, EV>) {
const skip = 0;
const limit = 20
const handleClick = () => {
fetchMore({ skip, limit, ...extraVariables }).then(() => { }) // okay
}
}
And the formerly acceptable call to fn()
is now unacceptable, because the fecthMore()
method expects it input to have more properties than fn()
is going to call it with:
fn({
extraVariables: { a: "hello" },
fetchMore(variables: { // error!
/* ~~~~~~~~~ <-- Property 'somethingElse' is missing in
type '{ a: string; } & PaginationVariables */
a: string, skip: number, limit: number, somethingElse: number[]
}) {
return Promise.resolve(variables.somethingElse.map(x => String(x)));
}
});
Upvotes: 1