在 TypeScript 中输入 mapShape 函数

Typing a mapShape function in TypeScript

我想使用这样的效用函数:

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

有没有办法在 TypeScript 中对其进行参数化,以便输出的类型是这样的?

{ foo: string, bar: number }

我试过这样做:

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
}

但是 TS 推断 out 的类型是 { foo: any, bar: any };它不会推断属性的特定类型。

下面产生了正确的输出类型,我只是不确定我是否可以参数化它:

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 }

我认为表现最好的打字是这样的:

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
}

我正在使用 inference from mapped types 以允许输出类型为 U 并且 mapper 对象是 U 键上的同态映射类型。

这会为 out 生成所需的输出类型,同时仍在映射器参数的回调属性中推断参数类型:

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

它还应该防止向映射器添加在要映射的对象中不存在的属性:

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

好的,希望对您有所帮助;祝你好运!

Playground link to code

在尝试@jcalz 的回答后,我得到了以下 lodash/fp 样式版本:

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',
})