使用打字稿

时间:2019-08-25 10:02:20

标签: javascript typescript functional-programming typescript-typings

是否可以使用Lodash样式的“ mapValues”和“ mapKeys”函数,该函数返回映射的对象文字-而不是通用的记录类型?

我的意思是

_.mapValues({a:1, b:2}, (v) => v === 1 ? 'aaa' : 'bbb') 

此代码(Lodash库)返回一个Record<'a' | 'b', 'aaa' | 'bbb'>,而不是文字类型{a: 'aaa', b: 'bbb'}

与Ramda / Fp-ts函数相同-某些类型信息丢失。

2 个答案:

答案 0 :(得分:3)

我认为TypeScript编译器没有为高阶类型分析提供足够的支持来为您执行此操作。我看到的问题:

  • 编译器没有很好的方法来推断函数const mapper = (v: 1 | 2) => v === 1 ? "aaa" : "bbb"是条件通用类型(如<V extends 1 | 2>(v: V) => V extends 1 ? "aaa" : "bbb"还是重载函数类型(如{(v: 1): "aaa", (v: 2): "bbb"})。如果希望编译器像对待函数那样处理,则必须手动声明或注释类型。

  • 即使可以推断出它们,也无法编写类似Apply<typeof f, typeof x>的类型函数,其中f是一个参数的重载或通用函数,并且x是可接受的参数,因此Apply<typeof f, typeof x>f(x)的类型。简称:there's no typeof f(x) in TypeScript。因此,尽管您可以调用 mapper(1),并且编译器知道结果为"aaa"类型,但是您无法在类型系统中表示该知识。这样可以防止您在类型系统中执行类似overloaded function resolutiongeneric function resolution的操作。


我能想到的最简单的_.mapValues键入将为您提供类似Record的宽类型,如果需要,您将不得不assert缩小类型:< / p>

declare namespace _ {
  export function mapValues<T, U>(
    obj: T,
    fn: (x: T[keyof T]) => U
  ): Record<keyof T, U>;
}
const obj = { a: 1, b: 2 } as const;
type ExpectedRet = { a: "aaa"; b: "bbb" };
_.mapValues(obj, v => (v === 1 ? "aaa" : "bbb")); // Record<"a"|"b", "aaa"|"bbb">
const ret = _.mapValues(obj, v => (v === 1 ? "aaa" : "bbb")) as ExpectedRet;

否则,您将不得不跳过许多熊熊的循环(手动指定类型,将函数手动声明为重载),最终会得到比类型断言更安全,更复杂的东西:

type UnionToIntersection<U> = (U extends any
  ? (k: U) => void
  : never) extends ((k: infer I) => void)
  ? I
  : never;

declare namespace _ {
  export function mapValues<T, U extends Record<keyof T, unknown>>(
    obj: T,
    fn: UnionToIntersection<{ [K in keyof T]: (x: T[K]) => U[K] }[keyof T]>
  ): U;
}

function mapper(v: 1): "aaa";
function mapper(v: 2): "bbb";
function mapper(v: 1 | 2): "aaa" | "bbb" {
  return v === 1 ? "aaa" : "bbb";
}

const obj = { a: 1, b: 2 } as const;
type ExpectedRet = { a: "aaa"; b: "bbb" };
const ret = _.mapValues<typeof obj, ExpectedRet>(obj, mapper);

不确定这是否值得解释...您必须在对_.mapValues的调用中手动指定输入和期望的输出类型,因为编译器无法推断出输出类型(如上所述)。您必须手动指定mapper是重载函数。 _.mapValues的键入很复杂,并且使用UnionToIntersection将所需的重载函数描述为将输入值转换为输出值的函数类型的交集。

所以,我会远离这个,只使用类型断言。


希望有所帮助;对不起,我没有更令人满意的答案。祝你好运!

Link to code

答案 1 :(得分:0)

我知道我在某处看到过它。但是我想你想要这个。

const mapped = Object.assign({}, ...Object.keys(ori).map((key) => {return {[key]: /*...*/}}))

示例:

JS:

const x = {a: 1, b: 2};
const y = Object.assign({}, ...Object.keys(x).map((key) => ({[key]: x[key] + 1})));

// y = {a: 2, b: 3}

TS:

const x: Record<string, number> = {a: 1, b: 2};
const y = Object.assign({}, ...Object.keys(x).map((key) => ({[key]: x[key] + 1})));