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> {
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;
// }