I want to convert an array of objects, into a set of known properties of a single object. I have working TypeScript code, but I want to make it into a generic function. Below is my working code.
global definition...
export type Pic = SanityDocument & {
id: Slug,
caption: string,
group: string,
pic: Image,
}
Example code I want to make generic:
const pics : Pic[] = await sanityClient.fetch(`
*[_type == "pic"]
`);
// Example type I want to make generic...
type PicMap = {alpha: Pic, bravo: Pic};
// How do I make `PicMap` be a generic I specify
// as part of a function call?
const picMap : PicMap = pics.reduce((acc, pic : Pic) => {
acc[pic.id.current as keyof PicMap] = pic;
return acc;
}, {} as PicMap);
The above code works. I want to modify the above code so that type PicMap becomes generic, so I can re-use this code for other target objects. How is that done? There are three scenarios of what I'm looking for:
- A TypeScript function that allows me to generically replace
PicMapwith something else. However, thePictype is fixed/required. This is my minimum requirement. - As before, but the
Pictype is also generic. - A variant of either option #1 or #2, where the
PicMaptype can be declared inline rather than requiring an explicit declaration.
Option #1 is what I absolutely need to fulfill this question. Options #2 and/or #3 are icing on the cake.
I am intentionally not using Record<t,k> for two reasons. First, I want the picMap object properties accessible with auto-complete. Second, for me it is ok if the source array has objects that don't exist as exposed properties of the final object. In other words, I am not attempting to limit what properties can be assigned...I am only limiting which properties I can "see" with intellisense. So I don't want a solution like this.
UPDATE
My own attempt at a solution currently looks like this:
New global definition...
type NamedAsset = SanityDocument & {
id: Slug // has a string property called 'current'
}
And then my generic function...
export function MapAsset<
V extends object,
K extends NamedAsset,
T extends K[]> (items:T) : V {
const acc = items.reduce((acc, asset:K) => {
acc[asset.id.current as keyof V] = asset;
return acc;
}, {} as V);
return acc;
}
Unfortunately the above has a "Type 'K' is not assignable" error. However, if the above were working then my example usage might look like this:
// Sample type for array contents...
export type Pic = NamedAsset & {
caption: string,
group: string,
pic: Image,
}
// Just some arbitrary way to fill the array.
const pics : Pic[] = await sanityClient.fetch(`
*[_type == "pic"]
`);
// An arbitrary type of arbitrary declared properties.
type PicMap = {alpha: Pic, bravo: Pic};
const picMap : PicMap = MapAsset(pics);
// Or, alternatively...
const picMap = MapAsset<PicMap>(pics);
UPDATE #2
I asked ChatGPT to solve the error I found in my prior example solution. What it gave back does work, but it is brain-melting. It's solution:
export function MapAsset<T extends NamedAsset[]>(items:T):
{
[K in T[number]['id']['current']]: Extract<T[number], {id : {current:K}}>
}
{
const acc = items.reduce((acc, asset) => {
acc[asset.id.current] = asset;
return acc;
}, {} as any);
return acc;
}
Then to use the above, I do this:
type PicMap = {alpha: Pic, bravo: Pic};
const picMap = MapAsset(pics) as PicMap;
Having a function whose result I must cast is not what I had in mind. However, I admit this would be acceptable-ish in the absence of a better answer. Aside from the need to cast the result, I also don't like that the MapAsset function is surprisingly esoteric.
typescript-genericstag. Why?SanityDocumentandsanityClient? Could you consolidate the code into a single minimal reproducible example without reference to private or third-party code? The goal is that we could copy and paste your plaintext into our own IDEs and see the issue for ourselves.