2

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.

Typescript playground

7
  • How do you want to generalize the relationship between "itemA" and "AStatus" and "A"? Is it just string manipulation like "item"+k, k+"Status" and k? Or is there some mapping somewhere? Commented Apr 14, 2022 at 13:51
  • If it's just string manipulation then maybe this approach would work for you. If that addresses the question fully, I can write up an answer; if not, what am I missing? Commented Apr 14, 2022 at 13:57
  • Thanks a lot! I not sure if it's the right way to use generics, because, the field to be updated is related to string itself, let me try it out. Commented Apr 14, 2022 at 14:01
  • @jcalz Hi! Please do write an answer, I think you solved my problem, tho not with this case regarding prisma, but a similar situation. But this approach seemed a bit hacky, in a situation where we want to update object data from a string input, what would you say? Commented Apr 14, 2022 at 14:16
  • Sorry, I don't understand. If you want to edit the question with a minimal reproducible example that demonstrates whatever situation you're talking about instead of the one currently in the question, maybe I could look at it. Or if you're happy with this and want to open a new question with the new situation, that would be fine too. But right now I don't really understand what "update object data from a string input" means enough to suggest anything. And can you explain what makes the approach above "hacky"? Is it the string manipulation? If so, what's the alternative... lookup table? Commented Apr 14, 2022 at 14:26

1 Answer 1

2

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.

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.