我正在尝试实现一个函数,该函数将Map类的所有内容序列化为对象。我的意思是我试图创建一个函数,如果接收到一个对象,它将检查每个属性,如果其中一些是一个Map,它将返回一个对象。同样,如果函数接收到一个Map数组,将返回一个对象数组。
我创建了这些类型:
type InputMapToObject =
| Map<keyof any, unknown>
| Record<keyof any, unknown>
| unknown[];
type ReturnMapToObject<T extends InputMapToObject> = T extends Map<
infer KMap extends keyof any,
infer VMap
>
? Record<
KMap,
VMap extends InputMapToObject ? ReturnMapToObject<VMap> : VMap
>
: T extends object
? {
[P in keyof T]: T[P] extends InputMapToObject
? ReturnMapToObject<T[P]>
: T[P];
}
: T extends (infer VArray)[]
? VArray extends InputMapToObject
? ReturnMapToObject<VArray>[]
: VArray[]
: never;
这种类型的作品完美,这里的测试,你可以看到,类型的工作:
interface TestingType {
testMap: Map<string, number>;
testArray: Array<number>;
testArray2: Array<Map<string, string>>;
}
const test: ReturnMapToObject<TestingType> = {
testMap: {
string: 0,
},
testArray: [0],
testArray2: [{ string: 'string' }],
};
我遇到的问题是在函数mapToObject
上(其他函数只是助手):
export function mapToObject<T extends InputMapToObject>(
itemToConvert: T
): ReturnMapToObject<T> {
if (itemToConvert instanceof Map) {
return [...itemToConvert.entries()].reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {} as ReturnMapToObject<T>);
}
if (Array.isArray(itemToConvert)) {
return itemToConvert.map((item) => {
if (isRecursiveValue(item)) {
return mapToObject(item);
}
return item;
});
}
return getObjectEntries(itemToConvert).reduce((acc, [key, value]) => {
if (isRecursiveValue(value)) {
acc[key] = mapToObject(value);
} else {
acc[key] = value;
}
return acc;
}, {} as ReturnMapToObject<T>);
}
function isRecursiveValue(item: any): item is InputMapToObject {
if (item instanceof Map) return true;
if (Array.isArray(item)) return true;
if (item && typeof item === 'object') return true;
return false;
}
function getObjectEntries<T extends Record<any, any>>(
value: T
): [keyof any, T[keyof T]][] {
return Object.entries(value) as [keyof any, T[keyof T]][];
}
我认为函数mapToObject
在JavaScript中可以正常工作,但是我遇到了类型错误。例如,我在第一个reducer上有一个错误,如果输入是一个Map,那么我使用reduce方法将条目转换为对象。这里的问题是,我试图输入初始对象,所以我是ReturnMapToObject<T>
,但TypeScript解释为T可以是任何东西,但不是,它只能是一个Map。
这是我在执行acc[key] = value
时得到的错误:
Element implicitly has an 'any' type because expression of type 'string | number | symbol' can't be used to index type 'object | unknown[] | Record<string | number | symbol, unknown>'.
No index signature with a parameter of type 'string' was found on type 'object | unknown[] | Record<string | number | symbol, unknown>'.ts(7053)
(parameter) acc: object | unknown[] | Record<string | number | symbol, unknown>
如果TypeScript正确识别Map示例中的T,如果acc为:Record<string | number | symbol, unknown>
,因为它在ReturnMapToObject
上定义,如果T扩展Map,那么它将返回一个Record。
1条答案
按热度按时间z9smfwbn1#
TypeScript不能通过控制流分析narrow或重新约束generic type parameters。因此,当您检查
itemToConvert instanceof Map
时,itemToConvert
的表观类型可以从T
缩小到(类似于)T & Map
,但类型参数T
* 本身 * 保持不变。这是一个很好的理由:你可以把类型看作是所有可能的可接受值的集合。让我们以
string
类型为例。如果我说值s
是泛型类型S extends string
,并且我检查s === "abc"
,现在我知道 values
是"abc"
,但我真的不知道关于 typeS
的任何更多信息。也许S
是字符串文字类型"abc"
,或者它可能是像"abc" | "def" | "ghi"
这样的类型的联合,或者它可能是string
。所有这些都与s === "abc"
一致。这就像如果我从一个袋子(类型)中取出一个大理石(值),然后检查大理石的颜色,这告诉了我很多关于大理石的信息,而关于袋子和它包含的大理石的颜色的信息却很少。因此,不能像缩小值的明显类型那样“缩小”泛型类型参数,至少在不向语言添加新功能的情况下不能这样做。在microsoft/TypeScript#27808中请求了一个这样的特性,在该特性中,您可以说
T oneof InputMapToObject
,这意味着T
被认为是InputMapToObject
的联合成员之一(假定互斥)。然后,当你检查是否是itemToConvert instanceof Map
时,你可以说T
本身是Map<keyof any, unknown>
或oneof Record<keyof any, unknown> | unknown[]
,其余的代码可能会按预期编译。但现在它不是语言的一部分。当你试图实现一个依赖于泛型类型参数返回conditional type的泛型函数时,这个限制是非常明显和痛苦的,就像你正在做的
ReturnMapToObject<T>
一样。在microsoft/TypeScript#33912上还有一个开放的通用条件函数实现的特性请求,同样,它还不是语言的一部分。除非这里有什么变化,否则你必须解决它。假设你不想完全重构,最方便的解决方法是使用类型Assert之类的东西来告诉编译器不要抱怨。这是合理的,如果你确定实现是正确的,但编译器不是。这也意味着你需要小心,因为如果你这样做,编译器不能捕捉到你的错误。
下面是一种进行Assert的方法:
您可以尝试
any
类型以外的更精确的类型,这可能会捕获一些严重的错误。和/或您可以使您的函数成为单调用签名重载,这也允许比调用签名更宽松的实现:有许多这样的变通方法,但是如果你想保留你当前的实现,所有这些都需要你放松一些类型检查,以避免这些(可能)错误的警告。
Playground链接到代码