打字稿推断的类型有时会解析为任何函数返回类型

时间:2018-10-06 20:00:03

标签: typescript

我正在尝试围绕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类型的解析有关,但是我真的不明白为什么它适用于SRCTARGET_MAPPINGSdate中都存在的属性, date2string),而不是仅存在于TARGET_MAPPINGS中的属性上。

我可以发现一个错误吗?

预先感谢您的帮助。

1 个答案:

答案 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不兼容。 编译器通过将类型更改为ReturnType来从故障中恢复;这是编译器未报告错误的错误。 Issue 25673具有相同的根本原因,因此我们可能应该将此案添加到该问题上,而不是提出新的案子。编辑:经过深思熟虑,我认为这与问题25673和any中的条件类型只是简化为其他情况,ReturnType

我试图为您的原始问题找到替代解决方案,但我无法让TypeScript推断适当的类型,而没有将地图分为两张地图,一个用于原始属性,一个用于计算属性。即使没有上述问题,使用any作为计算属性的never参数的类型也是不合理的,因为确实传递了该参数的值,大概是value。对于单个映射,我找不到找到TypeScript来推断undefined对于原始属性是value以及对于所有其他属性名称是SRC[ATTR]的方法。这是我想出的:

undefined

请注意,字段映射函数的参数的上下文类型输入仍然无法正常工作。我相信这是由于this TypeScript issue造成的,我建议您对这个问题投赞成票。