我正在尝试围绕JSON映射函数描述强大的类型安全约束。该函数将一个对象作为第一个参数,并使用作为第二个参数传递的映射函数返回该对象的映射表示。
从消费者的角度来看,像这样的合同:
let mappedResult = mapJson(
// Standard plain object literal coming, most of the time from serverside, generally described by an interface
// Let's call this type SRC
{ date: "2018-10-04T00:00:00+0200", date2: 1538604000000, aString: "Hello%20World", idempotentValue: "foo" },
// Applying some mapping aimed at converting input values above and change their type representation
// Rules are :
// - Keys should be a subset of SRC's keys, except for "new" computed keys
// - Values should be function taking SRC[key] and returning a new type NEW_TYPE[key] we want to capture in
// order to reference it in mapJson()'s result type
// Let's call this type TARGET_MAPPINGS
{ date: Date.parse, date2: (ts: number) => new Date(ts), aString: unescape, computed: (_, obj) => `${obj?`${obj.aString}__${obj.idempotentValue}`:''}` }
);
// Result type (NEW_TYPE) should be a map with its keys being the union of SRC keys and TARGET_MAPPINGS keys with following rules :
// - If key exists only in SRC, then NEW_TYPE[key] = SRC[key}
// - Otherwise (key existing in TARGET_MAPPINGS), then NEW_TYPE[key] = ResultType<TARGET_MAPPINGS[key]>
// In this example, expecting
// mappedResult = { date: Date.parse("2018-10-04T00:00:00+0200"), date2: new Date(1538604000000), aString: unescape("Hello%20World"), idempotentValue: "foo", computed: "Hello%20World__foo" }
// .. meaning that expected type would be { date: number, date2: Date, aString: string, idempotentValue: string, computed: string }
在一些帮助下(请参见this SO question),我设法使其主要用于以下类型:
type ExtractField<ATTR, T, FALLBACK> = ATTR extends keyof T ? T[ATTR] : FALLBACK;
type FunctionMap<SRC> = {
[ATTR in string]: (value: ExtractField<ATTR, SRC, never>, obj?: SRC) => any
}
type MappedReturnType<SRC, TARGET_MAPPINGS extends FunctionMap<SRC>> = {
[ATTR in (keyof TARGET_MAPPINGS | keyof SRC)]:
ATTR extends keyof TARGET_MAPPINGS ? ReturnType<Extract<TARGET_MAPPINGS[ATTR], Function>> : ExtractField<ATTR, SRC, never>
}
export function mapJson<
SRC extends object,
TARGET_MAPPINGS extends FunctionMap<SRC>
>(src: SRC, mappings: TARGET_MAPPINGS): MappedReturnType<SRC, TARGET_MAPPINGS> {
// impl .. not the point of the question
}
除了“计算”属性大小写被解析为类型为any
(而不是string
)的所有情况之外,一切看起来都很好
let mappedResult = mapJson(
{ date: "2018-10-04T00:00:00+0200", date2: 1538604000000, aString: "Hello%20World", idempotentValue: "foo" },
{ date: Date.parse, date2: (ts: number) => new Date(ts), aString: unescape, computed: (_, obj) => `${obj?`${obj.aString}__${obj.idempotentValue}`:''}` }
);
let v1 = mappedResult.date; // number, expected
let v2 = mappedResult.date2; // Date, expected
let v3 = mappedResult.aString; // string, expected
let v4 = mappedResult.idempotentValue; // string, expected
let v5 = mappedResult.computed; // any, NOT expected (expectation was string here !)
我猜想这与infer
类型的解析有关,但是我真的不明白为什么它适用于SRC
和TARGET_MAPPINGS
(date
中都存在的属性, date2
和string
),而不是仅存在于TARGET_MAPPINGS
中的属性上。
我可以发现一个错误吗?
预先感谢您的帮助。
答案 0 :(得分:2)
FunctionMap
根本没有达到您的预期。 TypeScript不支持将string
作为约束类型以及根据实际字符串而不同的属性类型的映射类型。如果您尝试声明这种映射类型,则编译器会将其转换为带有字符串索引签名的类型,并仅用string
替换所有出现的键变量,即:
type FunctionMap<SRC> = {
[ATTR: string]: (value: ExtractField<string, SRC, never>, obj?: SRC) => any
}
现在,由于string
不会为您使用的keyof SRC
类型扩展SRC
,因此value
参数的类型始终为never
。然后,当评估computed
的{{1}}属性的类型时,由于MappedReturnType<SRC, TARGET_MAPPINGS>
为ReturnType<Extract<TARGET_MAPPINGS[ATTR], Function>>
,并且Extract<TARGET_MAPPINGS[ATTR], Function>
的评估,(value: never: obj?: SRC) => any
的评估失败。与never
的约束(...args: any[]) => any
不兼容。 编译器通过将类型更改为编辑:经过深思熟虑,我认为这与问题25673和ReturnType
来从故障中恢复;这是编译器未报告错误的错误。 Issue 25673具有相同的根本原因,因此我们可能应该将此案添加到该问题上,而不是提出新的案子。any
中的条件类型只是简化为其他情况,ReturnType
。
我试图为您的原始问题找到替代解决方案,但我无法让TypeScript推断适当的类型,而没有将地图分为两张地图,一个用于原始属性,一个用于计算属性。即使没有上述问题,使用any
作为计算属性的never
参数的类型也是不合理的,因为确实传递了该参数的值,大概是value
。对于单个映射,我找不到找到TypeScript来推断undefined
对于原始属性是value
以及对于所有其他属性名称是SRC[ATTR]
的方法。这是我想出的:
undefined
请注意,字段映射函数的参数的上下文类型输入仍然无法正常工作。我相信这是由于this TypeScript issue造成的,我建议您对这个问题投赞成票。