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

How to type a Tuple with type inference in Typescript

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

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

有问题的函数是:

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].

我想阻止这种情况,但不确定如何操作。

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

我也将忽略 defaultVal 参数和接受它的柯里化函数,因为它使试图弄清楚什么类型的对象可能为其所有对象采用单个默认值的事情变得复杂特性。如果 T{ hello: string; world: number; } 并且我传入 [["hello"],["world"]],那么 defaultVal 是否同时是 stringnumber?正如我所说,我忽略了这一点。


所以我们将提出一个函数patch的签名,它接受一个通用类型的对象T和一个适合[=16的替换key-value元组列表=],returns 也是 T:

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

有趣的部分是 kvTuples 参数的类型。让我们首先处理最后的 | [] 。所有这一切只是给编译器 a hint to interpret kvTuples as a 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。你得到的是很多错误,这些错误并没有真正指出你应该修复的地方。

无论如何,kvTuples 是一个 Array<Something>,其中 Something 是一个 mapped type we do a lookup into. Let's examine that type. {[K in keyof T]: [K, T[K]?]} takes each property of T and turns it into a pair of key-value types (with the second element being 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)[] 类型,这与元组的并集不匹配。确实如此,但它会产生一个不幸的 last-line 错误消息:

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

这没有用,尤其是因为它谈论 "c" 对用户来说似乎没有任何理由。尽管如此,至少错误出现在正确的位置。毫无疑问,有可能使错误更加明显,但代价是 patch 的签名更加复杂,我担心它已经足够复杂了。

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

Link to code