The getStructToResponse function starts with an object of map functions, so the type of that object should be the generic and we'll work backwards from that to the input and output object types.
We define a general Mapper<In, Out> as an object with a property toResponse that maps a value from the In type to the Out type:
type Mapper<In, Out> = {
toResponse: (value: In) => Out
}
Given an object whose values are Mapper, we can work backwards to the input and output of the object that it maps:
type InputsFromMap<M> = {
[K in keyof M]: M[K] extends Mapper<infer In, any> ? In : never;
}
type OutputsFromMap<M> = {
[K in keyof M]: M[K] extends Mapper<any, infer Out> ? Out : never;
}
Our function is generic depending on a MapObj, which we say extends Record<keyof any, Mapper<any, any> so we know that every property is a Mapper.
It returns an object with a toResponse function, so it returns a Mapper. We use those inferred types to determine that we are returning Mapper<InputsFromMap<MapObj>, OutputsFromMap<MapObj>>.
We don't need to type objectOfValues because it is already known to be InputsFromMap<MapObj> from our function return. We do need to set an initial type for var result = {} as OutputsFromMap<MapObj> or else we won't be able to assign values to it because the key type won't be known. We also need to specify inside the forEach that the type for the key is keyof MapObj and not just string.
const getStructToResponse = <MapObj extends Record<keyof any, Mapper<any, any>>>(
objectOfFunctions: MapObj
): Mapper<InputsFromMap<MapObj>, OutputsFromMap<MapObj>> => ({
toResponse: (objectOfValues) => {
var result = {} as OutputsFromMap<MapObj>
Object.keys(objectOfFunctions).forEach((key: keyof MapObj) => {
result[key] = objectOfFunctions[key].toResponse(objectOfValues[key]);
})
return result
}
})
This gives us the errors that we want on invalid values. It doesn't give us error on excess properties, yet, but I'm just going to give up and hit "Post" because while I was typing this @jcalz came along and answered .
Edit: changed Object.keys(objectOfValues) to Object.keys(objectOfFunctions) as suggested by @jcalz. You still won't get errors on excess properties, but they'll be dropped from the result and won't cause errors.
Typescript Playground Link.