请注意,standard library 中已经有一个名为Record<K, V> 的类型,它代表一个对象类型,其键为K,其值为V。我要将您的类型重命名为MyRecord 以区分它们。事实上,内置的Record 类型非常有用,我可能最终(巧合地)在回答这个问题时使用它。
所以这是新代码:
interface MyRecord {
id: string;
createdBy: string;
dateCreated: string;
}
interface Result<R extends MyRecord> {
records: R[];
referredRecords: { [ref: string]: MyRecord; }
}
function mergeRecords<R extends MyRecord, K extends keyof R>(
result: Result<R>, refs: [K, string][]
) {
return result.records.map((record) => {
let refsObj = refs.reduce((acc, [refProp, toProp]) => {
let key: string = record[refProp]; // error, not a string
let val = result.referredRecords[key];
return Object.assign(acc, { [toProp]: val });
}, {});
return Object.assign(record, refsObj);
});
}
您的主要问题是您假设R[K] 将是string,但您没有告诉编译器。所以mergeRecords()的当前定义会很乐意接受这个:
interface Oops extends MyRecord {
foo: number;
bar: string;
}
declare const result: Result<Oops>;
mergeRecords(result, [["foo", "bar"]]); // okay, but it shouldn't accept "foo"!
mergeRecords(result, [["bar", "baz"]]); // okay
哎呀。
理想情况下,您应该限制R(可能还有K)类型,让编译器知道发生了什么。这是一种方法:
// return the keys from T whose property types match the type V
type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T];
function mergeRecords<
R extends MyRecord & Record<K, string>,
K extends KeysMatching<R, string>
>(
result: Result<R>, refs: [K, string][]
) {
return result.records.map((record) => {
let refsObj = refs.reduce((acc, [refProp, toProp]) => {
let key: string = record[refProp]; // no error now
let val = result.referredRecords[key];
return Object.assign(acc, { [toProp]: val });
}, {});
return Object.assign(record, refsObj);
});
}
现在R 不仅限于MyRecord,还限于Record<K, string>,因此键K 处的任何属性都必须具有string 类型。这足以消除let key: string = ... 语句中的错误。此外,K(对我们来说是多余的,但对编译器没有)受限于KeysMatching<T, string>,这意味着T 的键子集,其属性是string 值。这有助于调用函数时的 IntelliSense。
编辑:KeysMatching<T, V> 的工作原理...有不同的方法可以做到这一点,但上面的方法是使用 mapped type 迭代 T 的键 K 并创建一个新类型,其属性是T 的属性的函数。 -? modifier 使得新类型的每个属性都是必需的,即使 T 中的属性是可选的。具体来说,我使用conditional type 来查看T[K] 是否与V 匹配。如果是,则返回K;如果不是,则返回never。所以如果我们做了KeysMatching<{a: string, b: number}, string>,映射的类型就变成了{a: "a", b: never}。然后,我们将index into 映射到keyof T 的类型以获取值类型。所以{a: "a", b: never}["a" | "b"] 变成了"a" | never,也就是"a"。
现在让我们看看它的实际效果:
interface Oops extends MyRecord {
foo: number;
bar: string;
}
declare const result: Result<Oops>;
mergeRecords(result, [["foo", "bar"]]); // error on "foo"
mergeRecords(result, [["bar", "baz"]]); // okay
现在第一次调用失败了,因为"foo" 的属性不是string,而第二次调用仍然成功,因为"bar" 的属性很好。
好的,希望对您有所帮助。祝你好运!