我正在尝试在
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 编译器无法验证实现是否安全,最务实的方法是特别注意您的实现是否正确编写,然后使用 类型断言 告诉编译器值具有您声称具有的类型:
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.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。这是一个只使用类型断言并继续前进的好地方。
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 中的泛型参数,这被称为更高种类的类型