如何在Typescript中使用类型推断输入元组

时间:2019-06-21 19:02:03

标签: typescript typescript-generics

我正在尝试编写一个函数的类型签名,该函数将对象的值替换为该对象的更改列表。

我无法找到最准确的函数类型。

有问题的功能是:

export const patchObjFn = (
    defaultVal: any
) => <T extends object, K extends keyof T> (
    changeObj: Replacement<T>[] | Replacement<T>,
    moddingObj: T
) => {
    const moddedObj = cloned(moddingObj);
    const isSingleElement = changeObj.length !== 0
        && (changeObj.length === 1 || changeObj.length === 2)
        && !Array.isArray(changeObj[0]);
    const changes = isSingleElement
        ? [changeObj] as ([K] | [K, T[K]])[]
        : changeObj  as ([K] | [K, T[K]])[];
    for (const change of changes) {
        const [propName, val] = change.length === 1
            ? [change[0], defaultVal]
            : change;
        moddedObj[propName] = val;
    }
    return moddedObj;
};

我到达的类型是:

export type Replacement<T extends object, K extends keyof T> = [K] | [K, T[K]];

但这并不能真正起作用,因为如果K为'hello' | 'world'而T为{ hello: string; world: number; },我们可以传入['hello', 42]

我想防止这种情况,但是不确定如何做到。

1 个答案:

答案 0 :(得分:1)

我将只讨论类型而不是实现。这种类型的输入可能会导致一些错误出现在您的实现中。如果是这样,您很可能可以更改实现以安抚编译器,或者可以assert使实现与签名匹配。无论哪种情况,问题似乎都在于如何防止调用者传递不匹配的键/值对,而不是函数实现内部发生了什么。

我也将忽略defaultVal参数和使用它的curried函数,因为它使试图弄清楚哪种类型的对象可能为其所有属性采用单个默认值的事情变得复杂。如果T{ hello: string; world: number; }并且我通过了[["hello"],["world"]],那么defaultVal既是string又是number吗? ?正如我所说,我对此无视。


因此,我们将得出一个函数patch的签名,该函数采用一个通用类型T的对象和一个适合T的替换键值元组的列表,并且还返回类型为T的结果:

declare function patch<T extends object>(
  t: T,
  kvTuples: Array<{ [K in keyof T]: [K, T[K]?] }[keyof T]> | []
): T;

有趣的部分是kvTuples参数的类型。首先,让我们开始处理| []。所有这些操作使编译器a hint可以将kvTuples解释为tuple而不是普通数组。如果不选择该选项,则不会更改接受或不接受哪些输入,但是由于整个输入数组将被标记为错误,因此错误消息将变得完全不可理解:

patch({ a: "hey", b: 1, c: true }, [
  ["a", "okay"], // error! ?
  ["b", false], // error! ?
  ["c", true] // error! ?
]);
// string is not assignable to "a" | "b" | "c" ?

在上面,["b", false]是错误的条目,因为b属性的类型应为number。尽管您得到的是很多错误,但这些错误并没有真正指出您应该解决的问题。

无论如何,kvTuplesArray<Something>,其中Somethingmapped type,我们将lookup纳入其中。让我们检查一下这种类型。 {[K in keyof T]: [K, T[K]?]}接受T的每个属性,并将其变成一对键值类型(第二个元素为optional)。

因此,如果T{a: string, b: number, c: boolean},则映射类型为{a: ["a", string?], b: ["b", number?], c: ["c", boolean?]}。然后,我们使用查找类型来获取属性值类型的并集。因此,如果我们将映射类型称为M,那么我们正在做M[keyof T]M["a" | "b" | "c"]M["a"] | M["b"] | M["c"]["a", string?] | ["b", number?] | ["c", boolean?]。这就是我们想要的kvTuples每个元素的类型。

让我们尝试一下:

patch({ a: "hey", b: 1, c: true }, [
  ["a", "okay"],
  ["b", false], // error! ?
  ["c", true]
]);

错误至少在合理的位置出现。不太合理的是您收到的错误消息。以下至少是可以确定的:

// Type '(string | boolean)[]' is not assignable to type
// '["a", (string | undefined)?] | ["b", (number | undefined)?] |
// ["c", (boolean | undefined)?]'. ?

由于未进行类型检查,因此编译器显然在说["c", 7]的类型为(string | number)[],这与元组的并集不匹配。确实如此,但是不幸的是出现了错误消息的最后一行:

// Property "0" is missing  in type '(string | boolean)[]' 
// but required in type '["c", (boolean | undefined)?]' ?

这无济于事,特别是因为它在谈论"c"的原因似乎对用户来说毫无根据。尽管如此,至少错误显示在正确的位置。毫无疑问,可以使错误变得更加明智,但以patch签名的复杂性为代价,我担心它已经足够复杂了。

好的,希望能有所帮助。祝你好运!

Link to code