I want to update an object's value based on the input of a string that's related to an object key, how should I go about this with typescript?
const objData = { // random value
A: 11,
B: 13,
C: 53,
innerObj: {
AStatus: true,
BStatus: false,
CStatus: true,
},
};
type Item = 'itemA' | 'itemB' | 'itemC';
function processObj(item: Item, obj: typeof objData) {
if (item === 'itemA') { // <- duplicate
obj.A = 5;
obj.innerObj.AStatus = true;
return obj;
}
if (item === 'itemB') { // <- duplicate
obj.B = 5;
obj.innerObj.BStatus = true;
return obj;
}
if (item === 'itemC') { // <- duplicate
obj.C = 5;
obj.innerObj.CStatus = true;
return obj;
}
return
}
processObj('itemA', objData);
For instance, the input is itemA
, and only A
related objData gets updated.
CodePudding user response:
If the relationship between the item
parameter and the key names of obj
and obj.innerObj
is programmatically determined by string manipulation, such that there is a property key k
of obj
corresponding to a property k "Status"
of obj.innerObj
and to the value "item" k
of item
, then you can refactor your processObj
function to use template literal types. Template literal types allow you to represent some string literal manipulations at the type level. Here's one way to do it:
type Keys = Exclude<keyof typeof objData, "innerObj">;
// type Keys = "A" | "B" | "C"
function processObj(item: `item${Keys}`, obj: typeof objData) {
const k = item.substring(4) as Keys; // need to assert here
obj[k] = 5;
obj.innerObj[`${k}Status`] = true;
return obj;
}
The Keys
type uses the Exclude<T, U>
utility type to filter the keys of objData
to remove "innerObj"
, leaving us with the union "A" | "B" | "C"
.
For the rest of it, we're using template literal types to perform string concatenation at the type level. The type of item
is `item${Keys}`
, which evaluates to "itemA" | "itemB" | "itemC"
. We can calculate k
from item
by stripping the initial "item"
prefix from it; the result is of type Keys
, but the compiler can't verify that. Thus we just assert that k
is of type Keys
.
We can just set obj[k] = 5
with no compiler warning because the compiler understands that obj
has a number
property at all keys in Keys
. And we can also set obj.innerObj[`${k}Status`] = true
with no compiler warning, because the compiler understands that a template literal string value can have a template literal type, and that the type of `${k}Status`
is `${Keys}Status`
, which evaluates to "AStatus" | "BStatus" | "CStatus"
. And the compiler knows that obj.innerObj
has a boolean
property at those keys.
So this all works as desired.
On the other hand, if the relationship between item
and the keys of obj
and obj.innerObj
is arbitrary, then you can't necessarily use string manipulation to map between them. In this case, you would want something like a lookup table to represent the mapping, whatever it is. Such an implementation might look like this:
const propLookup = {
itemA: "A",
itemB: "B",
itemC: "C"
} as const;
const statusLookup = {
itemA: "AStatus",
itemB: "BStatus",
itemC: "CStatus"
} as const;
type Keys = keyof typeof propLookup;
// type Keys = "A" | "B" | "C"
function processObj(item: Keys, obj: typeof objData) {
obj[propLookup[item]] = 5;
obj.innerObj[statusLookup[item]] = true;
return obj;
}
The propLookup
and statusLookup
objects are just maps from valid item
values to the corresponding properties of obj
and obj.innerObj
. They are using const
assertions so the compiler keeps track of the string literal types of the values. If we left off as const
then the compiler would infer just string
for the values, which doesn't help us.
This also works as desired; the compiler understands that propLookup[item]
is a key of obj
with a number
value, and that statusLookup[item]
is a key of obj.innerObj
with a boolean
value.