Reputation: 8640
I want to use a utility function like this:
const out = mapShape(
{ foo: 1, bar: '2', baz: 'hello' },
{ foo: x => String(x), bar: x => parseInt(x) }
)
// outputs { foo: '1', bar: 2 }
Is there a way to parameterize it in TypeScript so that the type of the output will be this?
{ foo: string, bar: number }
I tried doing this:
export default function mapShape<
I extends Record<any, any>,
X extends { [K in keyof I]: (value: I[K], key: K, object: I) => any }
>(
object: I,
mapper: Partial<X>
): {
[K in keyof I]: ReturnType<X[K]>
} {
const result: any = {}
for (const key in mapper) {
if (Object.hasOwnProperty.call(mapper, key)) {
result[key] = (mapper[key] as any)(object[key], key, object)
}
}
return result
}
However type TS infers for out
is { foo: any, bar: any }
; it doesn't infer specific types for the properties.
The following produces the correct output type, I'm just not sure if I can parameterize it:
const mappers = {
foo: x => String(x),
bar: x => parseInt(x),
}
type outputType = {
[K in keyof typeof mappers]: ReturnType<typeof mappers[K]>
}
// { foo: string, bar: number }
Upvotes: 1
Views: 67
Reputation: 8640
After experimenting with @jcalz' answer, I got the following lodash/fp
style version to work:
export default function mapShape<U extends Record<any, (...args: any) => any>>(
mapper: U
): <T extends { [K in keyof U]?: any }>(
obj: {
[K in keyof T]?: K extends keyof U ? Parameters<U[K]>[0] : any
}
) => { [K in keyof U]: ReturnType<U[K]> } {
return (obj: any) => {
const result: any = {}
for (const key in mapper) {
if (Object.hasOwnProperty.call(mapper, key)) {
result[key] = mapper[key](obj[key], key, obj)
}
}
return result
}
}
mapShape({
foo: (x: number) => String(x),
bar: (x: string) => parseInt(x),
})({
foo: 1,
bar: '2',
baz: 'hello',
})
Upvotes: 0
Reputation: 327754
I think the typing that behaves the best is something like this:
function mapShape<T extends { [K in keyof U]?: any }, U>(
obj: T,
mapper: { [K in keyof U]: K extends keyof T ? (x: T[K]) => U[K] : never }
): U {
const result: any = {}
for (const key in mapper) {
if (Object.hasOwnProperty.call(mapper, key)) {
result[key] = (mapper[key] as any)(obj[key], key, obj)
}
}
return result
}
I'm using inference from mapped types to allow the output type to be U
and the mapper
object to be a homomorphic mapped type on the keys of U
.
This produces the desired output type for out
while still inferring the parameter types in the callback properties of the mapper argument:
const out = mapShape(
{ foo: 1, bar: '2', baz: 'hello' },
{ foo: x => String(x), bar: x => parseInt(x) }
)
/* const out: {
foo: string;
bar: number;
} */
It also should prevent adding properties to the mapper that don't exist in the object to be mapped:
const bad = mapShape(
{ a: 1 },
{ a: n => n % 2 === 0, x: n => n } // error!
// ------------------> ~ ~ <----------
// (n: any) => any is implicit any
// not never
)
Okay, hope that helps you proceed; good luck!
Upvotes: 3