Reputation: 1416
I have a type A with several properties which can be of different keys: strings, numbers, types B, irrelevant. I want to copy over certain fields from object a1 being type A to object a2, that is also of type A, but I do not know in advance which fields i want to copy over. The fields that I want to copy over I get in an array of string literals which tell me which field i want to copy.
type A = {
name: string,
length: number,
thing: B
}
const a1: A = {
name: "test",
length: 2,
thing: { whatever: true },
}
const a2: A = {
name: "",
length: 0,
thing: {whatever: false}
}
const propNames = ["name", "length"]
propNames.map(propName => a2[propName] = a1[propName as keyof A])
I would appreciate any help possible, here's the example on sandbox:
https://codesandbox.io/s/typescript-playground-export-fjol9
Upvotes: 0
Views: 137
Reputation: 1416
What I ended up using is casting my a1 and a2 objects as A & Record<string, any>
as that was the most elegant and for me understandable solution.
const a1 = {
name: "test",
length: 2,
thing: { whatever: true }
} as A & Record<string, any>;
const a2 = {
name: "",
length: 0,
thing: { whatever: false }
} as A & Record<string, any>;
arr.filter(propName => propName in a2).forEach(propName => (a2[propName] = a1[propName]));
Ofcourse, in the actual production solution I'm using the in
type operator to first of all check if the string is an actual field name in my object.
if("value" in a2)
etc..
Upvotes: 0
Reputation: 1847
reduce
for each property you want to copy, starting with an empty object.function copyPropertiesOver<E>(properties: string[], source: E, target: E): E {
const valuesToBeCopied = arr.reduce(
(acc, curr) => ({ ...acc, curr: source[curr as keyof E] }),
{}
);
const targetUpdated = ({ ...target, ...valuesToBeCopied } as unknown) as E;
return targetUpdated;
}
const a2Updated = copyPropertiesOver<A>(arr, a1, a2);
console.log(a2Updated);
Upvotes: 0
Reputation: 327624
If you write const arr = ["some", "strings"]
the compiler will infer that arr
is of type string[]
and it will immediately forget the exact number and values of its contents. This is often the correct behavior, since people do tend to mutate the contents of arrays (even ones which are const
can still have arr[0]="assigned";
and arr.push("pushed");
).
If you want the compiler to keep track of the particular string literal value types in an array literal you will need to change how you declare the variable. The easiest way to do this, const
assertions, was introduced in TypeScript 3.4:
const arr = ["name", "length"] as const;
Now the type of arr
is known to specifically be an array of length 2 whose first element must be "name"
and whose second element must be "length"
. From that, if you write arr.map(propName => ...)
, the compiler will know that propName
must be "name" | "length"
.
If you fix that, the second problem you'll face is this, assuming you're using TypeScript 3.5 or above:
arr.map(propName => a2[propName] = a1[propName]); // error!
// --------------> ~~~~~~~~~~~~
// Type 'string | number' is not assignable to type 'never'.
The problem here is that the compiler sees the assignment as possibly unsafe because it cannot verify that a2[propName]
is setting the same exact property that you're reading from a1[propName]
. It's obvious to us because the variable propName
is the same, but all the compiler sees is that these accesses use the same key type, which is a union of two possibilities, with four possible outcomes, like this:
const propName2 = arr[Math.random() < 0.5 ? 0 : 1];
const propName1 = arr[Math.random() < 0.5 ? 0 : 1];
a2[propName2] = a1[propName1]; // same error!
The only safe thing you could write to a2[propName]
when propName
is a union type is the intersection of all possible property types. In this case that's something which is both a string
and a number
, or string & number
, which is impossible, hence never
.
In the past I've suggested, see microsoft/TypeScript#25051 a possible way of asking the compiler to test each possible value of propName
to verify that it was being safe, but this is not likely to happen.
Luckily there's a way you can proceed here: use a generic callback to map()
:
arr.map(
<K extends typeof arr[number]>(propName: K) => (a2[propName] = a1[propName])
);
In this case, the compiler sees that you're assigning a value of type A[K]
to a value of type A[K]
. This is in theory just as "unsafe" as the prior code, but it's allowed by the compiler and is likely to continue to be so.. So that's how I'd recommend you proceed.
Okay, hope that helps; good luck!
Upvotes: 2