I have a function called updateMany
which accepts a generic type of an entity and allows to update certain data based on the entity and a "by" property which should be a keyof data
. Something like this:
function updateMany<Table>() {
return <
Data extends { [column in keyof Table]?: Table[column] },
By extends keyof Data
>(params: {
by: By;
data: Table[];
}) => {};
}
(I wrapped the function inside another function since I wasn't sure how to achieve it in a single function)
Now, everything seems to be working, except the fact that data
has to have all of the properties. When I try to wrap it inside Partial
, then I can pass to by
properties that doesn't exists in data
. I know that my implementation is incorrect, but this is as far as I could get it to what I want.
Play in playground
CodePudding user response:
My suggestion here would be to write updateMany()
like this:
function updateMany<T>() {
return <
K extends keyof T,
P extends K
>(params: {
by: P;
data: (Pick<T, K> & Partial<T>)[];
}) => { };
}
Both my version and yours use currying to work around the lack of "partial type parameter inference" (as requested in microsoft/TypeScript#26242), since there's no way to call a function with multiple type parameters like T
, K
, and P
and have the caller manually specify T
while letting the compiler infer K
and P
. Currently in TypeScript it's all or nothing; you can either manually specify all three, or let the compiler infer all three. Currying sidesteps this by having one function where you specify T
return another function where the compiler infers K
and P
.
Anyway, K
corresponds to the keys present on the elements of data
, and P
corresponds to the key specified in by
. I do this with two type parameters instead of just K
for both, since we don't want the compiler to infer that K
is the union of the key from by
and the ones in data
. Instead we constrain P extends K
so that whatever K
is inferred from data
, we require the key in by
to be in that set.
Conceptually we would have data
be of type Pick<T, K>[]
and that would be it. But since you said you wanted some better IntelliSense autocompletion for data
, I've intersected Pick<T, K>
with Partial<T>
. Luckily the compiler seems to use Pick<T, K>
to infer K
to be only those keys actually present in data
entries, while also using Partial<T>
to provide hints for autocompletion.
Let's see if it works:
const updateManyEntities = updateMany<Entity>();;
const X1 = updateManyEntities({ by: "A", data: [{ A: 1 }] }); // okay
const X2 = updateManyEntities({ by: "A", data: [{ A: 1, B: 2, C: 2 }] }); // okay
const X4 = updateManyEntities({ by: "C", data: [{ A: 1, B: 2 }] }); // error!
// ---------------------------> ~~
// Type '"C"' is not assignable to type '"A" | "B"'
const X3 = updateManyEntities({ by: "D", data: [{ A: 1, B: 2, C: 2 }] }); // error!
// ---------------------------> ~~
// Type '"D"' is not assignable to type '"A" | "B" | "C"'
const X5 = updateManyEntities({ by: "D", data: [{ A: 1, B: 2, C: 3, D: 4 }] }); // error!
// ---------------------------> ~~ ----> ~~~~
// Type '"D"' is not assignable to type 'keyof Entity' |
// Object literal may only specify known properties, and 'D' does not exist in type...
Looks good!