假设我有一个类似于Record的界面
interface Record {
id: string;
createdBy: string;
dateCreated: string;
}
和界面结果类似:
interface Result<R extends Record> {
records: R[];
referredRercords: { [ref:string]: Record; }
}
现在,每个类型R的记录都包含一些键K,该键K的字符串值与ReferedRecords中的其中一个引用相关。
我想要一些函数将它们合并为一个连贯的对象,例如:
mergeRecords<R extends Record, K extends keyof R>(result: Result<R>, refs: [K,string][]) {
return result.records.map((record) => {
let refsObj = refs.reduce((acc,[refProp, toProp]) => {
let key = record[refProp] as string; // ISSUE OCCURS HERE
let val = result.referredRecords[key]; // OR HERE IF CONVERSION EXCLUDED
return Object.assign(acc, {[toProp]: val});
},{});
return Object.assign(record, refsObj);
});
}
typescript抱怨类型R [K]不能与字符串充分重叠进行转换,并且如果我排除转换,它抱怨类型R [K]无法用于索引类型{[ref:string] :记录; }
这可行,但是我不喜欢它,因为它看起来很笨拙:
let key = record[refProp] as unknown as string;
有没有一种方法可以向打字稿保证类型R [K]确实是字符串,而无需事先进行未知转换?还是只是解决这个问题的方式?
编辑:
这也有效,我更喜欢它,所以除非有人有更好的东西,否则我会继续使用它。
let key = record[refProp];
let val = (typeof key === 'string') ? result.referredRecords[key] : undefined;
答案 0 :(得分:1)
请注意,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来迭代{{1 }}并创建一个新类型,其属性是K
的属性的函数。即使T
中的属性是可选的,-?
modifier也会使每个新类型的属性成为必需。具体来说,我使用了conditional type来查看T
是否与T
相匹配。如果是这样,则返回T[K]
;如果不是,则返回V
。因此,如果我们进行了K
,则映射的类型将变为never
。然后,我们index into将该类型与KeysMatching<{a: string, b: number}, string>
映射以获取值类型。因此{a: "a", b: never}
变为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"
处的属性很好,第二次调用仍然成功。
好的,希望能有所帮助。祝你好运!