【问题标题】:Enforcing the type of the indexed members of a Typescript object?强制 Typescript 对象的索引成员的类型?
【发布时间】:2012-10-30 04:32:56
【问题描述】:

我想将 string -> string 的映射存储在 Typescript 对象中,并强制所有键映射到字符串。例如:

var stuff = {};
stuff["a"] = "foo";   // okay
stuff["b"] = "bar";   // okay
stuff["c"] = false;   // ERROR!  bool != string

有没有办法强制我的值必须是字符串(或任何类型..)?

【问题讨论】:

    标签: typescript


    【解决方案1】:
    var stuff: { [key: string]: string; } = {};
    stuff['a'] = ''; // ok
    stuff['a'] = 4;  // error
    
    // ... or, if you're using this a lot and don't want to type so much ...
    interface StringMap { [key: string]: string; }
    var stuff2: StringMap = { };
    // same as above
    

    【讨论】:

    • number 也可以作为索引类型
    • 值得注意的是:编译器只强制执行值类型,而不是键类型。你可以做一些事情[15] = 'whatever',它不会抱怨。
    • 不,它确实强制执行密钥类型。即使 myObject 有一个很好的 toString() 实现,你也不能做 stuff[myObject] = 'whatever'。
    • @RyanCavanaugh 是否可以(或将)使用type Keys = 'Acceptable' | 'String' | 'Keys' 作为索引(键)类型?
    • 小心{ number: string },因为即使这可能会在分配时强制索引的类型,对象仍然在内部将键存储为string。这实际上会混淆 TypeScript 并破坏类型安全。例如,如果您尝试通过将键与值交换来将 { number: string } 转换为 { string: number },实际上您最终会得到 { string: string } 但 TypeScript 不会引发任何警告/错误
    【解决方案2】:
    interface AgeMap {
        [name: string]: number
    }
    
    const friendsAges: AgeMap = {
        "Sandy": 34,
        "Joe": 28,
        "Sarah": 30,
        "Michelle": "fifty", // ERROR! Type 'string' is not assignable to type 'number'.
    };
    

    这里,接口AgeMap 强制将键作为字符串,将值强制作为数字。关键字name 可以是任何标识符,应该用于建议您的接口/类型的语法。

    您可以使用类似的语法来强制对象对联合类型中的每个条目都有一个键:

    type DayOfTheWeek = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";
    
    type ChoresMap = { [day in DayOfTheWeek]: string };
    
    const chores: ChoresMap = { // ERROR! Property 'saturday' is missing in type '...'
        "sunday": "do the dishes",
        "monday": "walk the dog",
        "tuesday": "water the plants",
        "wednesday": "take out the trash",
        "thursday": "clean your room",
        "friday": "mow the lawn",
    };
    

    当然,您也可以将其设为泛型!

    type DayOfTheWeek = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";
    
    type DayOfTheWeekMap<T> = { [day in DayOfTheWeek]: T };
    
    const chores: DayOfTheWeekMap<string> = {
        "sunday": "do the dishes",
        "monday": "walk the dog",
        "tuesday": "water the plants",
        "wednesday": "take out the trash",
        "thursday": "clean your room",
        "friday": "mow the lawn",
        "saturday": "relax",
    };
    
    const workDays: DayOfTheWeekMap<boolean> = {
        "sunday": false,
        "monday": true,
        "tuesday": true,
        "wednesday": true,
        "thursday": true,
        "friday": true,
        "saturday": false,
    };
    

    2018 年 10 月 10 日更新: 在下面查看@dracstaxi 的答案 - 现在有一个内置类型 Record 可以为您完成大部分工作。

    1.2.2020 更新: 我已经从我的答案中完全删除了预制的映射接口。 @dracstaxi 的回答使它们完全无关紧要。如果您仍想使用它们,请查看编辑历史记录。

    【讨论】:

    • { [key: number]: T; } 不是类型安全的,因为对象的键在内部是 always 字符串——有关更多详细信息,请参阅@tep 对问题的评论。例如在 Chrome 中运行 x = {}; x[1] = 2; 然后 Object.keys(x) 返回 ["1"] 和 JSON.stringify(x) 返回 '{"1":2}'。 typeof Number 的极端情况(例如 Infinity、NaN、1e300、9999999999999999999999 等)被转换为字符串键。还要注意字符串键的其他极端情况,例如x[''] = 'empty string';x['000'] = 'threezeros';x[undefined] = 'foo'
    • @robocat 这是真的,我已经来回编辑了一段时间,从这个答案中删除了数字键控接口。最终我决定保留它们,因为 TypeScript 技术上具体地 允许数字作为键。话虽如此,我希望任何决定使用数字索引对象的人都能看到您的评论。
    • 公平地说这可以像这样改进吗:{ [name: string]: [age: number] } 包含数字值是年龄的提示? @SandyGifford
    • @Fasani 不幸的是不是 - 您刚刚定义的类型将是一个带有字符串作为键的对象,以及一个带有单个数字作为值的元组。但是,您可以使用该语法来提示元组中的值的用途!
    • @Fasani 看这里:typescriptlang.org/play?#code/…
    【解决方案3】:

    快速更新:从 Typescript 2.1 开始,有一个内置类型 Record&lt;T, K&gt;,其作用类似于字典。

    在这种情况下,您可以这样声明:

    var stuff: Record<string, any> = {};
    

    您还可以通过联合文字类型来限制/指定潜在的键:

    var stuff: Record<'a'|'b'|'c', string|boolean> = {};
    

    这是一个使用文档中记录类型的更通用的示例:

    // For every properties K of type T, transform it to U
    function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>
    
    const names = { foo: "hello", bar: "world", baz: "bye" };
    const lengths = mapObject(names, s => s.length);  // { foo: number, bar: number, baz: number }
    

    TypeScript 2.1 Documentation on Record&lt;T, K&gt;

    我看到在{[key: T]: K} 上使用它的唯一缺点是,您可以对有用的信息进行编码,说明您正在使用哪种密钥来代替“密钥”,例如如果您的对象只有主键,您可以这样提示:{[prime: number]: yourType}

    这是我为帮助进行这些转换而编写的正则表达式。这只会转换标签为“key”的情况。要转换其他标签,只需更改第一个捕获组:

    查找:\{\s*\[(key)\s*(+\s*:\s*(\w+)\s*\]\s*:\s*([^\}]+?)\s*;?\s*\}

    替换:Record&lt;$2, $3&gt;

    【讨论】:

    • 这应该得到更多的支持 - 它本质上是我答案的更新的原生版本。
    • 记录是否编译成{}
    • @DouglasGaskell 类型在编译后的代码中不存在。 Records(与 Javascript Maps 不同)仅提供了一种强制对象文字内容的方法。您不能将new Record...const blah: Record&lt;string, string&gt;; 编译为const blah;
    • 你无法想象这个答案对我有多大帮助,非常感谢:)
    • 值得一提的是,字符串联合也可以在Records 中工作:const isBroken: Record&lt;"hat" | "teapot" | "cup", boolean&gt; = { hat: false, cup: false, teapot: true };
    【解决方案4】:

    您可以将名称传递给未知键,然后编写您的类型:

    type StuffBody = {
      [key: string]: string;
    };
    

    现在您可以在类型检查中使用它:

    let stuff: StuffBody = {};
    

    但是对于FlowType,不需要有名字:

    type StuffBody = {
      [string]: string,
    };
    

    【讨论】:

      【解决方案5】:

      @Ryan Cavanaugh 的回答完全可以并且仍然有效。仍然值得补充的是,截至 16 年秋季,当我们可以声称大多数平台都支持 ES6 时,当您需要将某些数据与某些键相关联时,几乎总是更好地坚持 Map。

      当我们写let a: { [s: string]: string; } 时,我们需要记住,typescript 编译后没有类型数据之类的东西,它只是用于编译。和 { [s: string]: string; } 将编译为 {}。

      也就是说,即使你会写这样的东西:

      class TrickyKey  {}
      
      let dict: {[key:TrickyKey]: string} = {}
      

      这只是无法编译(即使对于target es6,你也会得到error TS1023: An index signature parameter type must be 'string' or 'number'.

      所以实际上你受限于字符串或数字作为潜在键,所以这里没有太多强制类型检查的意义,特别是请记住,当 js 尝试通过数字访问键时,它会将其转换为字符串。

      因此,即使键是字符串,也可以假设最佳实践是使用 Map 是相当安全的,所以我会坚持:

      let staff: Map<string, string> = new Map();
      

      【讨论】:

      • 不确定发布此答案时是否可行,但今天您可以这样做:let dict: {[key in TrickyKey]: string} = {} - 其中TrickyKey 是字符串文字类型(例如"Foo" | "Bar")。
      • 就我个人而言,我喜欢原生打字稿格式,但你是对的,最好使用标准。它为我提供了一种记录密钥“名称”的方法,该密钥实际上并不可用,但我可以将密钥命名为“patientId”之类的东西:)
      • 这个答案绝对有效,并且提出了很好的观点,但我不同意坚持使用原生 Map 对象几乎总是更好的想法。地图会带来额外的内存开销,并且(更重要的是)需要从存储为 JSON 字符串的任何数据手动实例化。它们通常非常有用,但不仅仅是为了从中获取类型。
      【解决方案6】:

      定义接口

      interface Settings {
        lang: 'en' | 'da';
        welcome: boolean;
      }
      

      强制键为设置界面的特定键

      private setSettings(key: keyof Settings, value: any) {
         // Update settings key
      }
      

      【讨论】:

        【解决方案7】:

        其实有一个内置实用程序Record

            const record: Record<string, string> = {};
            record['a'] = 'b';
            record[1] = 'c'; // leads to typescript error
            record['d'] = 1; // leads to typescript error
        

        【讨论】:

          【解决方案8】:

          基于@shabunc 的回答,这将允许强制执行键或值(或两者)成为您想要强制执行的任何内容。

          type IdentifierKeys = 'my.valid.key.1' | 'my.valid.key.2';
          type IdentifierValues = 'my.valid.value.1' | 'my.valid.value.2';
          
          let stuff = new Map<IdentifierKeys, IdentifierValues>();
          

          也应该使用 enum 而不是 type 定义。

          【讨论】:

            【解决方案9】:
            interface AccountSelectParams {
              ...
            }
            const params = { ... };
            
            const tmpParams: { [key in keyof AccountSelectParams]: any } | undefined = {};
              for (const key of Object.keys(params)) {
                const customKey = (key as keyof typeof params);
                if (key in params && params[customKey] && !this.state[customKey]) {
                  tmpParams[customKey] = params[customKey];
                }
              }
            

            如果您了解这个概念,请发表评论

            【讨论】:

            • 有没有办法让tmpParams[customkey] 具有适当的值?不只是any ?
            猜你喜欢
            • 1970-01-01
            • 2017-06-11
            • 1970-01-01
            • 2016-08-17
            • 2020-01-27
            • 2018-11-24
            • 2021-07-20
            • 2021-02-22
            相关资源
            最近更新 更多