Home > Back-end >  How to transform an object with TypeScript generics?
How to transform an object with TypeScript generics?

Time:10-22

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.

  • Related