Quiero usar una función de utilidad como esta:

const out = mapShape(
  { foo: 1, bar: '2', baz: 'hello' },
  { foo: x => String(x), bar: x => parseInt(x) }
)
// outputs { foo: '1', bar: 2 }

¿Hay alguna forma de parametrizarlo en TypeScript para que el tipo de salida sea este?

{ foo: string, bar: number }

Intenté hacer esto:

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
}

Sin embargo, el tipo TS infiere para out es { foo: any, bar: any }; no infiere tipos específicos para las propiedades.

Lo siguiente produce el tipo de salida correcto, simplemente no estoy seguro de si puedo parametrizarlo:

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 }
1
Andy 18 feb. 2020 a las 06:39

2 respuestas

La mejor respuesta

Creo que la escritura que se comporta mejor es algo como esto:

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
}

Estoy usando inferencia de los tipos mapeados para permitir que el tipo de salida sea U y el objeto mapper sea un tipo mapeado homomórfico en las teclas de U.

Esto produce el tipo de salida deseado para out sin dejar de inferir los tipos de parámetros en las propiedades de devolución de llamada del argumento del asignador:

const out = mapShape(
    { foo: 1, bar: '2', baz: 'hello' },
    { foo: x => String(x), bar: x => parseInt(x) }
)
/* const out: {
    foo: string;
    bar: number;
} */

También debería evitar agregar propiedades al mapeador que no existen en el objeto a mapear:

const bad = mapShape(
    { a: 1 },
    { a: n => n % 2 === 0, x: n => n } // error!
    // ------------------> ~  ~ <----------
    // (n: any) => any is             implicit any
    // not never
)

Bien, espero que eso te ayude a continuar; ¡buena suerte!

Enlace del área de juegos al código

3
jcalz 18 feb. 2020 a las 04:08

Después de experimentar con la respuesta de @jcalz, obtuve la siguiente versión de estilo lodash/fp para trabajar:

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',
})
0
Andy 18 feb. 2020 a las 06:59