强制Typescript对象的索引成员的类型?


289

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

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

有没有办法强制我值必须是字符串(或其他类型。)?

Answers:


518
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

13
这种语法的有趣之处在于,除了“ string”以外,其他任何类型的键实际上都是错误的。考虑到JS映射是由字符串显式键入键的,这是非常合理的,但是它确实使语法有些多余。像'{}:string'这样的东西来简单地指定值的类型似乎更简单,除非它们将以某种方式添加以允许自动强制键作为生成的代码的一部分。
Armentage,2012年

42
number也可以作为索引类型
Ryan Cavanaugh 2012年

3
值得注意的是:编译器仅强制执行值类型,而不强制执行键类型。你可以做东东[15] =“随便什么”,它不会抱怨。
2015年

3
不,它确实强制执行密钥类型。即使myObject具有不错的toString()实现,您也无法执行stuff [myObject] =“任何”。
AlexG '16

7
@RyanCavanaugh是否可以(或可能)使用a type Keys = 'Acceptable' | 'String' | 'Keys'作为索引(键)类型?
calebboyd

130
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的答案使它们完全无关。如果您仍要使用它们,请检查编辑历史记录。


1
{[key:number]:T; }不是类型安全的,因为对象的内部键始终是字符串-有关更多详细信息,请参见@tep的问题注释。例如,x = {}; x[1] = 2;在Chrome中运行,然后Object.keys(x)返回[“ 1”]并JSON.stringify(x)返回'{“ 1”:2}'。typeof Number(例如,Infinity,NaN,1e300、999999999999999999999等)的极端情况将转换为字符串键。另外注意的其它角落的情况下为字符串键一样x[''] = 'empty string';x['000'] = 'threezeros'; x[undefined] = 'foo'
robocat

@robocat这是真的,我反复编辑要从此答案中删除数字键界面。最终,我决定让他们因为打字稿技术专门允许数字,作为密钥。话虽如此,我希望任何决定使用带有数字索引的对象的人都能看到您的评论。
桑迪·吉福德

75

快速更新:自Typescript 2.1起,有一个内置类型Record<T, K>,其作用类似于字典。

来自docs的示例:

// 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文档 Record<T, K>

唯一的缺点我看到使用这种过度{[key: T]: K}的是,你可以在编码排序关键字是什么您使用的是地方的“钥匙”有用的信息例如,如果你的对象只有黄金键,你可以在那像这样的提示:{[prime: number]: yourType}

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

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

更换: Record<$2, $3>


3
这应该获得更多投票-本质上是我的答案的较新的本地版本。
桑迪·吉福德

记录会编译成{}吗?
道格拉斯·加斯凯尔

@DouglasGaskell类型在编译后的代码中不存在。 Records(不同于Javascript Map)仅提供一种强制执行对象文字内容的方法。您不能new Record...并且const blah: Record<string, string>;将编译为const blah;
桑迪·吉福德

您甚至都无法想象这个答案对我有多大帮助,非常感谢:)
Federico Grandi

值得一提的是,字符串联合在Records中也起作用: const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };
Sandy Gifford

10

@Ryan Cavanaugh的回答完全可以,而且仍然有效。值得一提的是,从16秋季开始,我们可以说大多数平台都支持ES6,因此只要需要将某些数据与某些键相关联,几乎总是最好坚持使用Map。

在撰写本文时,let a: { [s: string]: string; }我们需要记住,在编译完打字稿后就没有像类型数据这样的东西,它仅用于编译。和{[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();

4
不知道在发布此答案时是否可行,但是今天您可以执行以下操作:let dict: {[key in TrickyKey]: string} = {}- TrickyKey字符串文字类型(例如"Foo" | "Bar")在哪里。
罗曼·斯塔科夫

我个人喜欢本机打字稿格式,但是使用该标准是您的最佳选择。它为我提供了一种记录密钥“名称”的方法,该方法实际上并不可用,但是我可以将密钥命名为“ PatientId” :)
编码

这个答案是绝对有效的,而且很有意义,但是我不同意坚持原生Map对象总是更好的想法。映射会带来额外的内存开销,并且(更重要的是)需要从存储为JSON字符串的任何数据中手动实例化映射。它们通常非常有用,但并不是纯粹出于从中获取类型的目的。
桑迪·吉福德

7

定义界面

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

强制密钥成为“设置”界面的特定密钥

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

3

以@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定义。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.