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?
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With