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.
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
.
edit: noUncheckedIndexedAccess mitigates this issue and you can safely use just Record<string, string>
.
(statusMap as Record<string, string>)[anyString]
Works fine, but needs manually specifying the correct type on each access.
statusMap[anyString as keyof typeof statusMap]
This is kind of ugly and also unsafe - there's no guarantee that anyString is actually 'A' | 'S' | 'D'
.
Convenient, but not type safe, this just sidesteps the issue and the option has been deprecated since TypeScript 5.0.
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.
First, I would suggest using satisfies operator to type check whether statusMap
is a Record<string, string>
:
const statusMap = {
A: "Active",
S: "Suspended",
D: "Inactive",
} satisfies Record<string, string>;
Option 1:
We can define an object that will be a Record<"A" | "S" | "D" | string, string>
, however, if you would try defining an object with this type, you would get Record<string, string>
, since "A" | "S" | "D"
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 & {}
. {}
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: "Active",
S: "Suspended",
D: "Inactive",
} satisfies Record<string, string>;
const mappedStatusMap: Record<keyof typeof statusMap | (string & {}), string> =
statusMap;
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 => {
return typeof arg === "string" && arg in statusMap;
};
Usage:
if (isStatusMapKey(anyString)) {
statusMap[anyString];
}