TypeScript -在严格模式下使用字符串安全方便地访问对象/Map值

zqdjd7g9  于 2023-05-30  发布在  TypeScript
关注(0)|答案(3)|浏览(167)

考虑使用一个对象来翻译代码或进行国际化。

const statusMap = {
  'A': 'Active',
  'S': 'Suspended',
  'D': 'Inactive',
}

只要显式地使用键'A' | 'S' | 'D'statusMap.A),从这个对象获取值就可以正常工作,但是当处理未知字符串(statusMap[queryParams.status])时,我们会得到错误 “No index signature with a parameter of type 'string' was found on type”。这在纯JavaScript中有效,所以这是类型的问题。理想情况下,我希望statusMap[queryParams.status]返回string | undefined
使用任何字符串访问对象值的最方便和最安全的方法是什么?
有没有可能在不创建接口的情况下向这样的对象添加索引器?
这里有一些我已经考虑过的解决方案,但它们都有缺点。
1.将map转换为索引类型

const statusMap: Record<string, string | undefined> = {
  'A': 'Active',
  'S': 'Suspended',
  'D': 'Inactive',
}

statusMap[anyString]

这很好用,但是我们抛弃了自动完成(输入statusMap.不会再为我们提供A、S或D的建议)。另一个小问题是,即使'A'存在,statusMap.A也会导致string | undefined
此外,很难强制要求每个团队成员都正确地进行选角。例如,您可以只强制转换为Record<string, string>,而statusMap['nonexistent_key']将导致string类型,即使实际值是undefined
1.每次访问Map

(statusMap as Record<string, string | undefined>)[anyString]

这有相同的问题,确保每个团队成员都正确转换,需要在每次使用时重复。
1.每次访问时转换索引字符串

statusMap[anyString as keyof typeof statusMap]

这是一种丑陋的,也是不正确的-没有保证anyString实际上是'A' | 'S' | 'D'
1.在tsconfig中使用suppressImplicitAnyIndexErrors
方便,但不是类型安全的,这只是回避了这个问题,并且自TypeScript 5.0以来,该选项已被弃用。
1.效用函数

function getValSafe<T extends {}>(obj: T, key: string | number): T[keyof T] | undefined {
  return (obj as any)[key]
}

getValSafe(statusMap, anyString)

这工作得很好,但我不喜欢将自定义实用程序函数推到每个项目中的想法,只是为了在TypeScript中执行基本操作。
有没有更好的办法呢?
类似问题:this one使用了一个接口,但我想避免这种情况,想象一下有一个巨大的i18 nMap,甚至是一棵树。This question使用了一个更复杂的转换函数,我只是想像在常规JavaScript中一样使用对象Map,并且仍然具有类型安全和自动完成功能。

wtzytmuj

wtzytmuj1#

首先,我建议使用satisfies操作符来检查statusMap是否是Record<string, string>

const statusMap = {
  A: "Active",
  S: "Suspended",
  D: "Inactive",
} satisfies Record<string, string>;

选项1:我们可以定义一个Record<"A" | "S" | "D" | string, string>类型的对象,但是,如果你尝试定义一个Record<string, string>类型的对象,你会得到Record<string, string>,因为"A" | "S" | "D"string的一个子集,编译器会将键简化为string。为了防止这种情况,我们可以添加一个交集,而不是只添加stringstring & {} . {}不会打扰我们,因为string扩展了{},结果将是string,尽管它会阻止简化,这正是我们所需要的:

const statusMap = {
  A: "Active",
  S: "Suspended",
  D: "Inactive",
} satisfies Record<string, string>;

const mappedStatusMap: Record<keyof typeof statusMap | (string & {}), string> =
  statusMap;

Playground
选项2:创建一个自定义类型保护来检查statusMap中是否有任何字符串:

const isStatusMapKey = (arg: unknown): arg is keyof typeof statusMap => {
  return typeof arg === "string" && arg in statusMap;
};

用途:

if (isStatusMapKey(anyString)) {
  statusMap[anyString];
}

Playground

eqzww0vc

eqzww0vc2#

这些方法的问题在于,它们只是试图欺骗类型检查器接受不正确的值,并假装它们是正确的。这与创建严格类型检查的目的正好相反。(这是因为与Java不同,TS类型检查不会在运行时强制执行类型一致性。
因此,我们可以尝试在运行时实际检查anyString的值,对输入进行清理。
理论上这正是in类型保护的目的,不幸的是,当属性名不是常量时,类型检查器还不能解决这个问题。
所以你需要构建一个类型 predicate :

const isStatus = (str: string): str is keyof typeof statusMap => str in statusMap;

现在您可以执行以下操作:

const statusMsg = isStatus(anyString) ? statusMap[str] : undefined;

或者更好的方法是,不再返回undefined,而是停止处理输入并显示错误消息。或者使用默认的"unknown status"值。一旦你明确地处理了不正确的输入,选择权就掌握在你的手中。

3zwtqj6y

3zwtqj6y3#

使用satisfies

const statusMap = {
  'A': 'Active',
  'S': 'Suspended',
  'D': 'Inactive',
  'E': 12, // error
} as const satisfies Record<string,string>

statusMap.A // with autocomplete

相关问题