【问题标题】:Typescript, with types R and K extends keyof R, how to declare that R[K] is of type stringTypescript,类型 R 和 K 扩展 keyof R,如何声明 R[K] 是字符串类型
【发布时间】:2019-04-20 00:57:37
【问题描述】:

假设我有界面 Record 看起来像

interface Record {
  id: string;
  createdBy: string;
  dateCreated: string;
}

和界面结果如下:

interface Result<R extends Record> {
  records: R[];
  referredRercords: { [ref:string]: Record; }
}

现在,R 类型的每条记录都包含一些键 K,其字符串值与referredRecords 中的一个引用相关。

我想要一些函数将它们合并成一个连贯的对象,例如:

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] 不能用于索引 type { [ref:string] : 记录; }

这可行,但我不喜欢它,因为它看起来很老套:

let key = record[refProp] as unknown as string;

有没有一种方法可以确保 typescript 类型 R[K] 确实是一个字符串,而无需事先进行未知转换?或者这只是处理这个问题的方法?

编辑:

这也有效,我更喜欢它,所以除非有人有更好的东西,否则我会继续使用它:

let key = record[refProp];
let val = (typeof key === 'string') ? result.referredRecords[key] : undefined;

【问题讨论】:

    标签: typescript


    【解决方案1】:

    请注意,standard library 中已经有一个名为Record&lt;K, V&gt; 的类型,它代表一个对象类型,其键为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&lt;K, string&gt;,因此键K 处的任何属性都必须具有string 类型。这足以消除let key: string = ... 语句中的错误。此外,K(对我们来说是多余的,但对编译器没有)受限于KeysMatching&lt;T, string&gt;,这意味着T 的键子集,其属性是string 值。这有助于调用函数时的 IntelliSense。

    编辑:KeysMatching&lt;T, V&gt; 的工作原理...有不同的方法可以做到这一点,但上面的方法是使用 mapped type 迭代 T 的键 K 并创建一个新类型,其属性是T 的属性的函数。 -? modifier 使得新类型的每个属性都是必需的,即使 T 中的属性是可选的。具体来说,我使用conditional type 来查看T[K] 是否与V 匹配。如果是,则返回K;如果不是,则返回never。所以如果我们做了KeysMatching&lt;{a: string, b: number}, string&gt;,映射的类型就变成了{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" 的属性很好。

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

    【讨论】:

    • 很好的答案。非常感谢,非常彻底,将尽快测试并接受。如果这可行,我可能会问另一个关于输入返回值的问题,如果你认为你也可以帮忙的话。
    • 我得到了大部分内容,它确实有效,你能解释一下 KeysMatching 类型吗?具体是什么'-?部分意思和结尾的[keyof T]?
    猜你喜欢
    • 1970-01-01
    • 2021-05-03
    • 1970-01-01
    • 2021-10-17
    • 2017-02-12
    • 2011-03-06
    相关资源
    最近更新 更多