如何在 Typescript 中执行类型安全的对象映射?

问题描述 投票:0回答:2

我正在尝试在

Typescript
中编写一个映射对象的函数,同时保证它保留相同的键。我尝试了各种方法,但还没有找到有效的方法:

function mapObject1<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  return Object.fromEntries(
    Object.entries(object)
      .map(([key, value]): [K, B] => [key, mapper(value)]),
  ); // error!
  // Type '{ [k: string]: B; }' is not assignable to type '{ [P in K]: B; }'
}

export function mapObject2<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  const result: { [P in K]?: B } = {};

  (Object.keys(object) as K[]).forEach((key: K) => {
    result[key] = mapper(object[key]);
  });

  return result; // error!
  // Type '{ [P in K]?: B | undefined; }' is not assignable to type '{ [P in K]: B; }'
}

mapObject1
中,使用
Object.entries()
Object.fromEntries()
会导致按键类型转换为
string
。在
mapObject2
中,
result
的键必须是可选的,因为它一开始是空的,导致
Typescript
无法识别与
object
中存在的所有相同键。我应该如何解决这个问题?

typescript typing
2个回答
3
投票

不幸的是,由于一些原因,TypeScript 编译器无法验证实现是否安全,最务实的方法是特别注意您的实现是否正确编写,然后使用 类型断言 告诉编译器值具有您声称具有的类型:

function mapObject1<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  return Object.fromEntries(
    Object.entries(object)
      .map(([key, value]) => [key, mapper(value as A)]),
    // assert --------------------------------> ^^^^^
  ) as { [P in K]: B }
  //^^^^^^^^^^^^^^^^^^ <-- assert
}

function mapObject2<K extends PropertyKey, A, B>(
  object: { [P in K]: A },
  mapper: (value: A) => B,
): { [P in K]: B } {
  const result: { [P in K]?: B } = {};

  (Object.keys(object) as K[]).forEach((key: K) => {
    // assert -------> ^^^^^^ (you already did this)
    result[key] = mapper(object[key]);
  });

  return result as { [P in K]: B };
  // assert --> ^^^^^^^^^^^^^^^^^^
}

一切都编译得很好。


编译器无法遵循逻辑的原因:

  • Object.entries() method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries)

    和(
    Object.keys()
    方法
    )的类型不会将
    object
    的键限制为您的
    通用类型K
    。TypeScript中的对象类型不是“密封的”,因此对象可能具有比编译器所知更多的属性。因此,编译器仅针对键类型返回 
    string
    ,针对值类型返回 
    unknown
    。有关更多信息,请参阅 
    Object.keys 在 TypeScript 中不返回 keyof 类型?信息。

    所以你遇到的一些错误是编译器试图让你避免这样的问题:

    interface Foo { x: number, y: number, z: number } const obj = { x: Math.LN2, y: Math.PI, z: Math.E, other: "abc" }; const foo: Foo = obj; // this assignment is okay const oops = mapObject1(foo, num => num.toFixed(2)); /* const oops: { x: string; y: string; z: string; } */ // 💥 num.toFixed is not a function !!!

    foo

     的类型为 
    Foo
    ,因为有额外的属性就可以了。但随后您在映射函数中遇到了运行时错误。实际上,这通常是一种相当不寻常的情况,因此您可能确信这是一个可以接受的风险。如果是这样,请使用类型断言并继续。如果没有,那么您将需要重写函数以接受要转换的显式键列表。但我认为这超出了所提出问题的范围。

  • 即使编译器知道/认为

    Object.keys(object)

     返回了每个键,它也无法理解循环这些键并在返回对象中设置属性将导致返回对象从部分对象提升到一个完整的对象。这样做是
    安全(在某种程度上Object.keys()
    不会错过任何必需的属性键),但验证这一点超出了编译器的推理能力。有关更多详细信息,请参阅
    如何在不使用 Typescript 进行转换的情况下从 Partial 移至 T。这是一个只使用类型断言并继续前进的好地方。

Playground 代码链接


0
投票

function mapObject<R>( object: object, mapper: (val: any, key: string) => any, ): R { return Object.fromEntries( Object.entries(object).map(([key, value]) => [key, mapper(value, key)]), ) as any; }

然后您可以按如下方式调用此函数,例如从输入对象的每个值中省略属性 
c

const input = {
  k1: {
    b: 1,
    c: 2,
  },
  k2: {
    c: 3,
    d: 4,
  },
} as const;

const mapper = <T extends { c: number }>(value: T) => {
  const { c, ...other } = value;
  return other;
};

const result = mapObject<
  { [k in keyof typeof input]: ReturnType<typeof mapper<typeof input[k]>> }
>(input, mapper);

最后,
typeof result

将是:

{
  readonly k1: {
    readonly b: 1;
  };
  readonly k2: {
    readonly d: 4;
  };
}

这是必要的根本原因是你无法将泛型参数传递给 Typescript 中的泛型参数,这被称为
更高种类的类型

(请参阅这个答案)。

© www.soinside.com 2019 - 2024. All rights reserved.