Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I Object.assign to sub-property of an object in Typescript?

Tags:

typescript

At 1st I create an object principalLegal, then I add to it's address# properties all of the properties of address constant.
I do this because I don't want to manually declare many times over all the address properties inside of principalLegal.address#.

const address = {
  index: {
    id: "index",
    text: "Index",
  },
  country: {
    id: "country",
    text: "Country",
  },
  // Many other fields here
};

const principalLegal = {
  _id: "principalLegalData",
  text: "Principal Data"
  name: {
    id: "principalName",
    text: "Principal name"
  },
  address1: {
    _id: "principalAddress1",
    text: "Principal Address 1"
  },
  address2: {
    _id: "principalAddress2",
    text: "Principal Address 2"
  },
  // address3 and so on..

}

Object.assign(principalLegal.address1, address);
Object.assign(principalLegal.address2, address);

But, when I try to access principalLegal.address1.country, Typescript says
Property 'country' does not exist on type '{ _id: string; _text: string; }'.
Which means that Typescript ignores all the Object.assign operations.

What is the way to make this right, without repeating address properties many times over and over inside of principalLegal?

like image 896
avalanche1 Avatar asked Dec 21 '25 15:12

avalanche1


1 Answers

TypeScript doesn't allow a variable's type to change arbitrarily... so if you were removing properties or changing a property type from one type to another, the only way to deal with this would be to use a new variable for each mutation. This is a viable (but not the only) option for adding (or refining) properties too, so here's one way you'd do it:

function modifyProperty<T, K extends keyof T, U>(
  obj: T,
  key: K,
  modify: (oldValue: T[K]) => U
) {
  let ret = obj as any;
  ret[key] = modify(obj[key]);
  return ret as { [P in keyof T]: P extends K ? U : T[P] };
}

The function modifyProperty(obj, key, modify) takes an object obj and one of its keys key, and a modify callback that converts the old value to the new value. Here's how one might use it:

const principalLegal1 = modifyProperty(principalLegal, "address1", a =>
  Object.assign(a, address)
);
principalLegal1.address1.country; // okay

const principalLegal2 = modifyProperty(principalLegal1, "address2", a =>
  Object.assign(a, address)
);
principalLegal2.address1.country; // okay
principalLegal2.address2.country; // okay

Notice how after each mutation with modifyProperty you throw away your existing principalLegal variable and use the returned value instead. This is possibly clunky, but it is consistent with a static type system in which the type of a variable cannot change.


Now luckily for you, you are not changing the type of principalLegal arbitrarily... instead you are narrowing it by adding subproperties. And TypeScript does have the ability to use control flow analysis to narrow the types of variables.

Unfortunately there's currently no built-in way in TypeScript for a function call to just narrow the type of one of its arguments (although this is slated to change in TS3.7 with the asserts type predicate modifier). So there's no direct way to say that Object.assign(foo, bar) will narrow foo itself.


UPDATE

The asserts modifier just got merged to master so the nightly builds support it now. If you install typescript@next locally, you can do something like this and see that it works:

function assign<T, U>(target: T, source: U): asserts target is T & U {
  Object.assign(target, source);
}

assign(principalLegal.address1, address);
principalLegal.address1.country; // okay

assign(principalLegal.address2, address);
principalLegal.address1.country; // okay
principalLegal.address2.country; // okay

For now, you can use a user-defined type guard that does the assignment and returns true. When you then check the return value and throw if it is not true, the compiler will understand that the type of the variable has been unconditionally narrowed. It's a bit of an abuse of type guards, and the asserts modifier should hopefully fix this soon.

Here's the function:

function assign<T, U>(x: T, y: U): x is T & U {
  Object.assign(x, y);
  return true;
}

And here's how we'd use it:

if (!assign(principalLegal.address1, address)) throw new Error();
principalLegal.address1.country; // okay

if (!assign(principalLegal.address2, address)) throw new Error();
principalLegal.address1.country; // okay
principalLegal.address2.country; // okay

This is a bit annoying but probably less annoying than discarding variables; it's up to you, I guess.


Okay, hope that helps; good luck!

Link to code

like image 98
jcalz Avatar answered Dec 24 '25 10:12

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!