TypeScript – 在严格模式下安全、方便地使用字符串访问对象/映射值

huangapple go评论117阅读模式
英文:

TypeScript - accessing object/map values using strings in strict mode safely and conveniently

问题

考虑创建一个对象,用于翻译代码或国际化。

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

当使用键 &#39;A&#39; | &#39;S&#39; | &#39;D&#39; 显式地获取此对象的值时(statusMap.A),一切正常,但在处理未知字符串时(statusMap[queryParams.status]),我们会收到错误消息:“找不到带有类型 'string' 的参数的索引签名”。在普通 JavaScript 中,这是有效的,所以这是一个与类型相关的问题。理想情况下,我希望 statusMap[queryParams.status] 返回 string | undefined

访问对象值的最方便和安全的方式是什么?

是否可以在对象中添加类似这样的索引器而无需为其创建接口?

已考虑过的一些解决方案如下,但它们都存在缺点。

1)将映射转换为索引类型

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

statusMap[anyString]

这很好用,但我们失去了自动完成(输入 statusMap. 将不再为 A、S 或 D 提供建议)。另一个小问题是 statusMap.A 将导致 string | undefined,即使 &#39;A&#39; 是存在的。

此外,很难强制执行每个团队成员都正确进行转换。例如,您可能只将其转换为 Record<string, string>,而 statusMap['nonexistent_key'] 将导致 string 类型,即使实际值为 undefined

2)在每次访问时将映射转换为索引类型

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

这与确保每个团队成员执行正确转换的相同问题,需要在每次使用时重复。

3)在每次访问时将索引字符串转换

statusMap[anyString as keyof typeof statusMap]

这有点丑陋,而且也是不正确的 - 不能保证 anyString 实际上是 &#39;A&#39; | &#39;S&#39; | &#39;D&#39;

4)在 tsconfig 中使用 suppressImplicitAnyIndexErrors

方便,但不安全,这只是规避了问题,而且自 TypeScript 5.0 起,此选项已被弃用。

5)实用函数

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

getValSafe(statusMap, anyString)

这相当不错,但我不喜欢将自定义实用函数塞入每个项目中,只是为了在 TypeScript 中执行基本操作。

有没有更好的方法来做到这一点?

类似的问题:这个问题 使用了接口,但我想避免使用它,想象一下有一个庞大的 i18n 映射或甚至是一棵树。这个问题 使用了更复杂的翻译函数,我只想像在常规 JavaScript 中一样使用对象映射,同时还能保持类型安全和自动完成。

英文:

Consider having an object for translating codes or for internationalization.

const statusMap = {
  &#39;A&#39;: &#39;Active&#39;,
  &#39;S&#39;: &#39;Suspended&#39;,
  &#39;D&#39;: &#39;Inactive&#39;,
}

Getting values from this object works fine as long as you use the keys &#39;A&#39; | &#39;S&#39; | &#39;D&#39; explicitly (statusMap.A), but when dealing with an unknown string (statusMap[queryParams.status]), we get the error "No index signature with a parameter of type 'string' was found on type". This works in plain JavaScript, so it's an issue with types. Ideally I would like statusMap[queryParams.status] to return string | undefined.

What's the most convenient and safe way of accessing object values using any string?

Is it possible to add an indexer to an object like this without creating an interface for it?


Here are some solutions I've already considered, but all of them have drawbacks.

  1. Casting the map to an indexed type
const statusMap: Record&lt;string, string | undefined&gt; = {
  &#39;A&#39;: &#39;Active&#39;,
  &#39;S&#39;: &#39;Suspended&#39;,
  &#39;D&#39;: &#39;Inactive&#39;,
}

statusMap[anyString]

This works fine, but we just threw away autocompletion (typing statusMap. won't get us suggestions for A, S, or D anymore). Another small issue is that statusMap.A would result in string | undefined even though &#39;A&#39; is there.

Moreover, it's difficult to enforce that every team member does the casting correctly. For example you could cast to just Record&lt;string, string&gt; and statusMap[&#39;nonexistent_key&#39;] would result in a string type, even though the actual value is undefined.

  1. Casting the map on every access
(statusMap as Record&lt;string, string | undefined&gt;)[anyString]

This has the same problem of ensuring every team member does correct casts, needs to be repeated on every usage.

  1. Casting the indexing string on every access
statusMap[anyString as keyof typeof statusMap]

This is kind of ugly and also incorrect - there's no guarantee that anyString is actually &#39;A&#39; | &#39;S&#39; | &#39;D&#39;.

  1. Using suppressImplicitAnyIndexErrors in tsconfig

Convenient, but not type safe, this just sidesteps the issue and the option has been deprecated since TypeScript 5.0.

  1. Utility function
function getValSafe&lt;T extends {}&gt;(obj: T, key: string | number): T[keyof T] | undefined {
  return (obj as any)[key]
}

getValSafe(statusMap, anyString)

This works quite well, but I don't like the idea of shoving a custom utility function into every single project just to do a basic operation in TypeScript.


Is there any better way of doing this?

Similar questions: this one uses an interface, but I want to avoid that, imagine having a huge i18n map or even a tree. This question uses a more complex translation function, I just want to use object maps like I would in regular JavaScript and still have type safety and autocompletion.

答案1

得分: 2

以下是翻译好的部分:

许多这些方法的问题在于它们试图欺骗类型检查器,使其接受不正确的值并假装它们是正确的。这与严格的类型检查的初衷相反。(这是因为与Java不同,TS类型检查不会在运行时强制执行类型一致性。)

因此,我们可以尝试在运行时实际检查anyString的值,对输入进行清理。

从理论上讲,这正是in类型保护的创建目的,不幸的是,类型检查器目前无法在属性名称不是常量时正确处理它。

所以你需要构建一个类型谓词:

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

现在你可以这样做:

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

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

英文:

The problem with many of these approaches is that they just try to trick the type checker into accepting incorrect values and pretending that they're correct. Which is the opposite of what strict type checking was created for. (This is because unlike say in Java, TS type checking doesn't enforce type conformity at runtime.)

So we can try to actually check the value of anyString at runtime instead, sanitising the input.

In theory this is exactly what the in type guard was created for, unfortunately the type checker can't work it out yet when property name is not a constant.

So you need to build a type predicate:

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

Now you can do:

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

Or even better, instead of returning undefined, you can stop processing the input and display an error message. Or use a default &quot;unknown status&quot; value. Once you handle incorrect inputs explicitly, the choice is in your hands.

答案2

得分: 2

首先,我建议使用 satisfies 运算符来检查 statusMap 是否是一个 Record&lt;string, string&gt; 类型:

const statusMap = {
  A: "Active",
  S: "Suspended",
  D: "Inactive",
} satisfies Record&lt;string, string&gt;;

选项1:
我们可以定义一个对象,该对象将是 Record&lt;&quot;A&quot; | &quot;S&quot; | &quot;D&quot; | string, string&gt; 类型,但是,如果您尝试使用此类型定义一个对象,您将得到 Record&lt;string, string&gt;,因为 &quot;A&quot; | &quot;S&quot; | &quot;D&quot; string 的子集,编译器会简化键为 string。为了防止这种情况发生,我们可以添加一个交集而不只是添加 string,我们可以添加 string &amp; {}{} 不会影响我们,因为 string 扩展 {},结果仍然是 string,但它会防止简化,这正是我们所需要的:

const statusMap = {
  A: "Active",
  S: "Suspended",
  D: "Inactive",
} satisfies Record&lt;string, string&gt;;

const mappedStatusMap: Record&lt;keyof typeof statusMap | (string &amp; {}), string&gt; =
  statusMap;

选项2:
创建一个自定义类型守卫,用于检查 statusMap 中是否包含任何字符串:

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

用法:

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

playground

playground

英文:

First, I would suggest using satisfies operator to type check whether statusMap is a Record&lt;string, string&gt;:

const statusMap = {
  A: &quot;Active&quot;,
  S: &quot;Suspended&quot;,
  D: &quot;Inactive&quot;,
} satisfies Record&lt;string, string&gt;;

Option 1:
We can define an object that will be a Record&lt;&quot;A&quot; | &quot;S&quot; | &quot;D&quot; | string, string&gt;, however, if you would try defining an object with this type, you would get Record&lt;string, string&gt;, since &quot;A&quot; | &quot;S&quot; | &quot;D&quot; is a subset of string and the compiler will simplify the keys to just string. To prevent this, instead of adding just string, we can add an intersection: string &amp; {}. {} doesn't bother us, since string extends {} and the result will be string anyway, though it will prevent from simplifying which is exactly what we need:

const statusMap = {
  A: &quot;Active&quot;,
  S: &quot;Suspended&quot;,
  D: &quot;Inactive&quot;,
} satisfies Record&lt;string, string&gt;;

const mappedStatusMap: Record&lt;keyof typeof statusMap | (string &amp; {}), string&gt; =
  statusMap;

playground

Option 2:
Create a custom type guard to check whether any string is in the statusMap:

const isStatusMapKey = (arg: unknown): arg is keyof typeof statusMap =&gt; {
  return typeof arg === &quot;string&quot; &amp;&amp; arg in statusMap;
};

Usage:

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

playground

答案3

得分: 0

你需要使用 satisfies

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

statusMap.A // 带有自动完成
英文:

You need to use satisfies:

const statusMap = {
  &#39;A&#39;: &#39;Active&#39;,
  &#39;S&#39;: &#39;Suspended&#39;,
  &#39;D&#39;: &#39;Inactive&#39;,
  &#39;E&#39;: 12, // error
} as const satisfies Record&lt;string,string&gt;

statusMap.A // with autocomplete

huangapple
  • 本文由 发表于 2023年5月24日 17:37:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/76322112.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定