【问题标题】:Mapping object with correct type narrowing in mapper function在映射器函数中使用正确的类型缩小映射对象
【发布时间】:2021-12-16 23:37:31
【问题描述】:

我正在将一个项目移植到 typescript 中,除了这个简单的实用功能之外,一切都很好:

function mapObject(obj, mapperFn) {
  return Object.fromEntries(
    Object.entries(obj).map(([key, value]) => mapperFn(key, value))
  );
}

它用于转换对象的键和值,如下所示:

let obj = { a: 1, b: 2 };
let mappedObj = mapObject(obj, (key, value) => [
  key.toUpperCase(),
  value + 1,
]);

mappedObj; // { A: 2, B: 3 }

edit 为了清楚起见,输出对象的条目值不一定与输入对象中的条目值相同,例如上面的示例可以将数字转换为字符串以生成 { A: '2', B: '3' },或者只为每个数字返回 null:{ A: null, B: null }。另外,为了简单起见,我只考虑输入和输出对象中的字符串键,因为这就是它在我们项目中的使用方式。

我希望mapperFn 保留对象条目的值类型,支持按键类型缩小,就像GetEntryOf 在下面的示例中所做的一样:

type GetEntryOf<T> = {
  [K in keyof T]: [K, T[K]];
}[keyof T];

interface TestI {
  str: string;
  num: number;
}

let entry: GetEntryOf<TestI>;

if (entry[0] === 'str') {
  entry[1]; // string
} else if (entry[0] === 'num') {
  entry[1]; // number
} else {
  entry; // never
}

这是我最接近的,不幸的是我需要过渡到使用key in obj 运算符:

function mapObject<K>(
  obj: K,
  mapperFn: (key: keyof K, value: K[typeof key]) => [string, unknown]
): Record<string, unknown> {
  const newObj = {} as Record<string, unknown>;

  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      const value = obj[key];
      const [newKey, newValue] = mapperFn(key, value);

      newObj[newKey] = newValue;
    }
  }

  return newObj;
}

interface TestI {
  str: string;
  num: number;
}

let test: TestI;

mapObject(test, (key, value) => {
  if (key === 'str') {
    return [key, value]; // value should be string
  } else if (key === 'num') {
    return [key, value]; // value should be number
  } else {
    return [key, value]; // value should be never
  }
});

如您所见,value 的类型没有正确缩小:

考虑到GetEntryOf 可以做到这一点,我认为应该可以在mapperFn 内部做同样的事情。在当前的打字稿中这甚至可能吗?如果可以,那么合适的方法是什么?

注意:工作类型缩小示例基于this SO answer。我知道并且可以接受同一个答案中提到的约束:“不能保证该值也没有其他属性”。

【问题讨论】:

  • 能否请您提供几个您期望的输出示例
  • 所以根本问题是valuestring | number 类型(在你的例子中),不管你缩小key 吗?我怀疑你不能用mapObject 的当前签名解决这个问题。如果您可以将其定义为接受一个元组而不是两个离散参数,我怀疑您可以。但这将涉及更改现有代码以接受元组而不是离散参数。
  • @t-j-crowder 确切地说,我希望 value 类型从 if 分支中的 string | number 缩小,在 else 分支中导致类型 never。作为一个相对新手的打字员,我不确定我是否理解“接受元组而不是离散参数”的含义,但如果我理解正确,你是说如果不更改 fn 签名就无法完成,因此它是如何实现的调用。我可以将签名从(key, value) 更改为(entry) 或任何其他不会过多改变整个函数调用方式的东西。你能提供一个元组调用的例子吗?
  • @T.J.Crowder 我搞砸了 ping。有关答案/问题,请参见上文。 :)
  • (A tuple 基本上是一个固定长度的数组,其中元素有自己的类型。[number, number] 是一个元组。)我是说我不确定确定 i> 它可以在不切换到元组的情况下完成。 :-) 可悲的是,我在使用元组时也没有太多运气,但我觉得这是可能的。 @captain-yossarian 对 TS 的了解比我多得多,希望他能提供帮助。

标签: typescript object mapping


【解决方案1】:

我认为您正在寻找这样的东西:

type Values<T> = T[keyof T]

type MakeTuple<T> = Values<{
  [Prop in keyof T]: { key: Prop, value: T[Prop] }
}>

type Output<T> = {
  [Prop in keyof T]: [Prop, T[Prop]]
}


const mapObject = <K,>(
  obj: K,
  mapperFn: (params: MakeTuple<K>) => [MakeTuple<K>['key'], MakeTuple<K>['value']]
) => (Object.keys(obj) as Array<keyof K>)
  .reduce((acc, key) => {
    const [newKey, newValue] = mapperFn({ key, value: obj[key] });

    return {
      ...acc,
      [newKey]: newValue
    }

  }, {} as Output<K>)


interface TestI {
  str: string;
  num: number;
}


declare let test: TestI;


const result = mapObject(test, (obj) => {
  // const key = params[0]
  // const value = params[1]
  if (obj.key === 'str') {
    return [obj.key, obj.value]
  } else if (obj.key === 'num') {
    return [obj.key, obj.value];
  } else {
    throw new Error();
  }
});

result.num // ['num', number]

Playground

我不确定你为什么使用Object.prototype.hasOwnProperty.call(obj, key),但你可以使用:

const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
  : obj is Obj & Record<Prop, unknown> =>
  Object.prototype.hasOwnProperty.call(obj, prop);

相反。 你甚至可以像我一样重载hasOwnProperty here

更新 您将被允许在

中使用两个单独的参数


const result = mapObject(test, (key, value) => {
  if (key === 'str') {
    return [key, obj]
  } else if (key === 'num') {
    return [key, obj];
  } else {
    throw new Error();
  }
});

何时合并此 PR Control flow analysis for destructured discriminated unions

【讨论】:

  • 不错!但是 ughreduce 上。获胜的简单循环。 :-)
  • @T.J.Crowder 我知道简单的循环更快。就像reduce more :D Nest time 将使用简单循环)))
  • 这不是关于速度,而是关于易读性和不必要的、容易出错的复杂性。¹但是如果你喜欢它... :-) (¹在函数式编程之外使用预定义的、可重用的 reducer .)
  • 我注意到很多人不喜欢reduce
  • 回答我的问题之前的三个 cmets:1)我讨厌 hasOwnProperty,它只是因为使用了 in 运算符才需要,这是我在问你们之前解决这个问题的最佳选择2)我应该明确表示输出的类型不相关,或者,更好地说:无法知道值类型,因为它可以从映射器 fn 中返回任何值 3)我同意 reduce不应该用来避免混淆和减少读者的精神负担。也就是说,我可以很快找到完美的解决方案,我已经重构了大部分项目并且它正在创造奇迹。 1000x
【解决方案2】:

基于captain-yossarian's answer,这是将要实现的最终结果:

type Values<T> = T[keyof T];

type MakeTuple<T> = Values<{
  [Prop in keyof T]: { key: Prop; value: T[Prop] };
}>;

interface MapObjectMapper<K> {
  (params: MakeTuple<K>): [string, unknown];
}

export default function mapObject<K>(
  obj: K,
  mapperFn: MapObjectMapper<K>
): Record<string, unknown> {
  return Object.fromEntries(
    (Object.keys(obj) as Array<keyof K>).map((key) =>
      mapperFn({ key, value: obj[key] })
    )
  );
}

为了便于阅读,我提取了映射器函数签名,并将输出类型替换为[string, unknown],以说明问题中的编辑注释(仅使用字符串键)以及返回值确实不能已知。

【讨论】:

    猜你喜欢
    • 2022-12-23
    • 2020-07-21
    • 2015-11-03
    • 2019-09-24
    • 2020-05-16
    • 2020-08-02
    • 2022-06-22
    • 1970-01-01
    • 2019-01-22
    相关资源
    最近更新 更多