0

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?

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks! Knew I was over-complicating this one :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.