Reputation: 521
I'd like to know if it's possible to create a function that can map one generic object to another, using custom transformations. Something like:
interface Input {
str: string;
num: number;
}
interface Output {
str: number;
num: string;
}
where...
const input: Input = {
str: "78",
num: 1
}
const output: Output = transform(transformations)(input);
// where 'transformations' is a bunch of callbacks to provide custom transformation
E.g. 2 such callbacks might look like
type Transform<T, R> = (val: T) => R;
const transformString: Transform<string, number> = (val) => parseFloat(val);
const transformNumber: Transform<number, string> = (val) => `${val}`;
Here is a playground link of what I currently have: https://tsplay.dev/wEVQ3N
Upvotes: 0
Views: 234
Reputation: 328302
While TypeScript doesn't directly support higher kinded types of the sort needed to express completely arbitrary type transformations (as requested in microsoft/TypeScript#1213), you might be able to compose a few basic transformations that meet your needs.
Let's start with your Transform<I, O>
function type that takes an input of type I
and produces an output of type O
:
type Transform<I, O> = (val: I) => O;
If you have an object type T
whose properties are each Transformation
s, you can use that to create a Transform<I, O>
where I
is an object type with the same keys as T
and whose property values come from all the transformation input types, and where O
is an object type with the same keys as T
and whose property values come from all the transformation output types. Let's call this MapObject<T>
:
type MapObject<T extends Record<keyof T, Transform<any, any>>> = Transform<
{ [K in keyof T]: T[K] extends Transform<infer I, any> ? I : never },
{ [K in keyof T]: T[K] extends Transform<any, infer O> ? O : never }
>;
For example, MapObject<{a: Transform<V, W>, b: Transform<X, Y>}>
will be a Transform<{a: V, b: X}, {a: W, b: Y}
. And you can make a function mapTransforms()
that takes a transformMap
of type T
and produces an output of type MapObject<T>
, by iterating through the object entries of transformMap
:
function mapTransforms<T extends Record<keyof T, Transform<any, any>>>(
transformMap: T
): MapObject<T> {
return (x: any): any =>
Object.fromEntries((Object.entries(x) as Array<[keyof T, any]>)
.map(([k, v]) => [k, transformMap[k](v)]));
}
Here I've used any
a few times to stop the compiler from complaining about the implementation of mapTransforms()
. While you can express "T
→ MapObject<T>
" in the type system, it's hard/impossible to get the compiler to actually verify that a particular function implementation is of that type. Using any
circumvents that... meaning I have to take care that the implementation is correct; the compiler won't be able to catch mistakes here.
To test this, let's define three functions of type Transform<string, number>
, Transform<number, string>
, and Transform<boolean, A<boolean>>
:
const str2Num = (x: string) => +x;
const num2Str = (x: number) => "" + x;
const bool2A = (x: boolean): A<boolean> => ({ name: x });
and call mapTransforms()
to turn them into a Transform<Input, Output>
:
const inputToOutput: Transform<Input, Output> = mapTransforms({
x: str2Num,
y: num2Str,
z: bool2A,
nest: mapTransforms({
a: num2Str
})
});
That compiles with no error, so the compiler at least thinks that the output of mapTransforms()
is of the desired type. And we can test it with your example at runtime:
const input: Input = {
x: "22",
y: 33,
z: false,
nest: {
a: 66
}
}
const output = inputToOutput(input);
console.log(JSON.stringify(output));
// {"x":22,"y":"33","z":{"name":false},"nest":{"a":"66"}}
Looks good!
Upvotes: 2
Reputation: 521
Here is the approach I used to solve this, however it requires a set of defined transformations (e.g. in a Transformations
object) to achieve it. This doesn't allow me to add new transformations easily, as any changes must be reflected in the Transformations
object as well a couple of other types as well, and then handled specifically in createGenericObjectMapper
.
I'll leave this solution here for anyone who might find it valuable.
Upvotes: 0