Consider having an object for translating codes or for internationalization.
const statusMap = {
'A': 'Active',
'S': 'Suspended',
'D': 'Inactive',
}
Getting values from this object works fine as long as you use the keys 'A' | 'S' | 'D'
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.
- Casting the map to an indexed type
const statusMap: Record<string, string | undefined> = {
'A': 'Active',
'S': 'Suspended',
'D': 'Inactive',
}
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 'A'
is there.
Moreover, it's difficult to enforce that every team member does the casting correctly. For example you could cast to just Record<string, string>
and statusMap['nonexistent_key']
would result in a string
type, even though the actual value is undefined
.
- Casting the map on every access
(statusMap as Record<string, string | undefined>)[anyString]
This has the same problem of ensuring every team member does correct casts, needs to be repeated on every usage.
- 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 'A' | 'S' | 'D'
.
- Using suppressImplicitAnyIndexErrors in tsconfig
Convenient, but not type safe, this just sidesteps the issue and the option has been deprecated since TypeScript 5.0.
- Utility function
function getValSafe<T extends {}>(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.