Home > Net >  Create a function with a specific mapped type as return type
Create a function with a specific mapped type as return type

Time:09-06

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?

CodePudding user response:

Narrow the type of relations, then proceed to use the inferred tuple in the return type like this:

export function ResponseTransformer<T, R>(
  obj: T,
  relations: [R] extends [[]] ? [] : { [K in keyof R]: Extract<R[K], keyof T> }
): TransformedResponse<T, R extends (keyof T)[] ? R[number] : keyof T> {

Playground


A little more detail on the narrowing can be found here.

CodePudding user response:

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

  • Related