Search code examples
typescripttypescript-genericstypescript-types

Create a function with a specific mapped type as return type


I am trying to create a method that iterates over an object and replaces every key-value pair where the value extends { _id: Types.ObjectId } with a modified key and maps the value to the string representation of _id. So, for example

{
  "name": "test",
  "image": {
    "_id": "63162902546ac59fb830ccae",
    "url": "https://..."
  }
}

would be transformed to

{
  "name": "test",
  "imageId": "63162902546ac59fb830ccae"
}

Additionally, it should be possible to specify keys which don't get modified. So, if I'd pass image in the example above, the object would remain unchanged.

So far, I have created a mapped type that looks as follows

type TransformedResponse<T, R extends keyof T> = {
  [K in keyof T as K extends R
    ? K
    : T[K] extends { _id: Types.ObjectId }
    ? `${string & K}Id`
    : K]: K extends R
    ? T[K]
    : T[K] extends { _id: Types.ObjectId }
    ? string
    : T[K];
}

and seems to produce the expected types:

interface Example {
  name: string;
  image: {
    _id: Types.ObjectId;
    url: string;
  };
}
type T1 = TransformedResponse<Example, 'name'>; // { name: string; imageId: string; }
type T2 = TransformedResponse<Example, 'image'>; // { name: string; image: { _id: Types.ObjectId; url: string; } }

The function that performs the actual transformation looks as follows

export function ResponseTransformer<T>(
  obj: T,
  relations: (keyof T)[]
): TransformedResponse<T, keyof T> {
  const transformedObj = {};
  Object.keys(obj).forEach((key) => {
    if (obj[key] && obj[key]._id && !relations.includes(key as keyof T)) {
      transformedObj[`${key}Id`] = obj[key].toString();
    } else {
      transformedObj[key] = obj[key];
    }
  });
  return transformedObj as TransformedResponse<T, keyof T>;
}

A call of this function, such as

const example = {
  name: 'test',
  image: {
    _id: '63162902546ac59fb830ccae',
    url: 'test',
  },
};
const t1 = ResponseTransformer(example, ['name']); 

does not produce the expected type T1 (see above) but actually a generic response (TransformedResponse<T, keyof T>). In the places where I call the function, the exact type T1 is required as the return type, which causes type checking to fail.

How can I make the function return the specific type?

Additionally: How can I make the type's generic argument R optional?


Solution

  • The parameter relations must be its own generic type. Otherwise it is just keyof T but not the actually values you passed to the function.

    export function ResponseTransformer<T, K extends keyof T>(
      obj: T,
      relations: K[]
    ): Expand<TransformedResponse<T, K>> {
      return {} as any
    }
    
    type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
    

    TypeScript has some problems displaying the correct return type. But adding Expand fixes this.

    Also, don't forget to actually pass an ObjectId to the function and not a string.

    const example = {
      name: 'test',
      image: {
        _id: '63162902546ac59fb830ccae' as unknown as Types.ObjectId,
        url: 'test',
      },
    };
    const t1 = ResponseTransformer(example, ['name']); 
    // const t1: {
    //     name: string;
    //     imageId: string;
    // }
    

    Playground