When the connection between the item
parameter and the key names within obj
and obj.innerObj
is determined programmatically through string manipulation, where a property key k
of obj
corresponds to a property k+"Status"
of obj.innerObj
and to the value "item"+k
of item
, you can optimize your processObj
function using template literal types. Template literal types enable you to depict certain string manipulations at the type level. Here's a 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 employs the Exclude<T, U>
utility type to filter the keys of objData
by eliminating "innerObj"
, leaving us with the union
"A" | "B" | "C"
.
Furthermore, we utilize template literal types to conduct string concatenation at the type level. The type of item
is `item${Keys}`
, resulting in
"itemA" | "itemB" | "itemC"
. We derive
k
from
item
by removing the initial
"item"
prefix, leading to a type of
Keys
, although the compiler cannot confirm that. Hence, we simply
assert that
k
aligns with type
Keys
.
We assign obj[k] = 5
without any compilation warning because the compiler recognizes that obj
contains a number
property across all keys within Keys
. Similarly, setting
obj.innerObj[`${k}Status`] = true
doesn't trigger a compiler warning either, as the compiler comprehends that a
template literal string value has a template literal
type, and that the type of
`${k}Status`
equates to
`${Keys}Status`
, which evaluates to
"AStatus" | "BStatus" | "CStatus"
. Also, the compiler acknowledges that
obj.innerObj
features a
boolean
property at those specific keys.
Therefore, this process functions according to expectations.
Conversely, if the correlation between item
and the keys present in obj
and obj.innerObj
is arbitrary, then utilizing string manipulation for mapping purposes might not be feasible. In such instances, employing a lookup table to embody the mapping structure would be more suitable. An implementation may resemble the following:
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 represent mappings from valid item
values to respective properties within obj
and obj.innerObj
. By incorporating const
assertions, the compiler retains awareness of the string literal types associated with these values. Without as const
, the compiler would deduce just string
instead of providing meaningful insights.
This method also operates as intended; the compiler interprets propLookup[item]
as a key of
obj</code containing a <code>number
value, while recognizing that
statusLookup[item]
pertains to a key within
obj.innerObj</code featuring a <code>boolean
value.
Playground link to code