What I'm trying to do
This one is tough to explain in words, so here's a base case example of what I'm attempting to do.
I have my "base" type:
type Profile = {
firstName: string
lastName: string
image: string // some relative url path
}
I have some logic that will then transform this object from Profile
=> ProfileEnhanced
with the additional requirement that any extra properties are "passed through"
type ImageEnhanced = {
src: string
width: number
height: number
}
type ProfileEnhanced = {
firstName: string
lastName: string
image: ImageEnhanced
}
// My desired fn stub
type MapFn = <T extends Profile>(profile: T) => MapToEnhancedProfile<T>
The tricky part is that I need my function to accept any object that extends Profile
and return the type of the passed object with the enhancements/transformations. For example, in the example below, I need the property address
to be passed through in the return type.
How I've attempted to solve this
I have solved this with the following typing, but it feels to me like an anti-pattern. Is there a cleaner way to do this?
type Profile = {
firstName: string
lastName: string
image: string // some relative url path
}
type ImageEnhanced = {
src: string
width: number
height: number
}
type ProfileEnhanced = {
firstName: string
lastName: string
image: ImageEnhanced
}
// Is this a reasonable approach??
type TransformProfile<TProfile extends Profile> = {
[key in keyof Pick<TProfile, 'image'>]: ImageEnhanced
} & {
[key in keyof Omit<TProfile, 'image'>]: TProfile[key]
}
// Dummy fn
async function getImage(src: string): Promise<ImageEnhanced> {
return Promise.resolve({
src: '/some/image.png',
width: 200,
height: 150
})
}
async function mapProfile<T extends Profile>(profile: T): Promise<TransformProfile<T>> {
// Some async fn that resolves the image dimensions from the source path
const enhancedImage = await getImage(profile.image)
return {
...profile,
image: enhancedImage
}
}
// Example usage
(async function(){
const profileWithAddress: Profile & { address: string } = {
firstName: 'John',
lastName: 'Doe',
image: '/some/image.png',
address: '123 Street'
}
const enhancedProfile = await mapProfile(profileWithAddress)
// It works! (but feels hacky to me)
type OutputType = keyof typeof enhancedProfile // "image" | "firstName" | "lastName" | "address"
})()
My Question
Although I have solved this, it feels like an anti-pattern solution to me. Is there a better way to approach this?
CodePudding user response:
There's no point in Pick
-ing image
only the use it for the key. You should also be using Omit
for the part of the profile that doesn't change:
type TransformProfile<TProfile extends Profile> = {
image: ImageEnhanced;
} & Omit<TProfile, "image">;
Basically, you are using the utility types only to get the keys, but actually, the utility types gives you what you need already.