The following code block produces a typescript error, because although we know that foo[k]
and bar[k]
are the same type, TS cannot know (well, maybe by some magic it could, but apparently it doesn't)
interface IMixed {
a: number;
b: string;
c: boolean;
}
const foo: IMixed = { a: 1, b: 'one', c: true };
const bar: IMixed = { a: 2, b: 'two', c: false };
(Object.keys(foo) as Array<keyof IMixed>).forEach(k => {
foo[k] = bar[k];
// ^^^^^^ Type 'string | number | boolean' is not assignable to type 'never'.
});
When TS can't figure something out that I know to be true, I cast it. But in this case, although I know this is a valid assignment, I don't know which type it is ... all I know is they are the same. I couldn't quite work out an elegant way to use a generic here.
Assume I am committed to the practice of iterating over these properties (say, because we expect properties do be added down the line, and similar code exists all over the codebase ...)
In short: How do I assert that this assignment is valid?
(side question: why does the error refer to the assignee as type 'never'?)
CodePudding user response:
It is worth using reduce
instead of forEach
in this case:
interface IMixed {
a: number;
b: string;
c: boolean;
}
const foo: IMixed = { a: 1, b: 'one', c: true };
const bar: IMixed = { a: 2, b: 'two', c: false };
(Object.keys(foo) as Array<keyof IMixed>)
.reduce((acc, elem) => ({
...acc,
[elem]: bar[elem]
}), foo);
Mutations does not work well in TypeScript.
See related questions: first, second, third, forth and my article
UPDATE
(Object.keys(foo) as Array<keyof IMixed>).forEach(k => {
foo[k] = bar[k]; // error
});
You have an error here, because forEach
as well as reduce
is dynamic.
bar[k]
is a union of number | string | boolean
, as well as foo[k]
. It means that inside the loop it would be possible to assign foo['a'] = foo['c']
. Both values are valid, since we expect a union of number | string | boolean
but it also would be unsafe. That's why TS forbids this behavior.
From the other hand, reduce
works, because we create new object which extends IMixed
instead of mutating
UPDATE
In order to be able to mutate foo
you need add indexing
to IMixed
interface:
type IMixed= {
a: number;
b: string;
c: boolean;
[prop: string]: number | boolean | string
}
const foo: IMixed = { a: 1, b: 'one', c: true };
const bar: IMixed = { a: 2, b: 'two', c: false };
(Object.keys(foo) as Array<keyof IMixed>).forEach(k => {
foo[k] = bar[k];
});
CodePudding user response:
captain-yossarian usefully (as always) points out that you can't rely on the Object.keys(foo).forEach
to pick valid keys, because it provides all of the keys of foo
, but foo
could be a supertype of IMixed
that has properties bar
doesn't have. Also, just because foo
and bar
both have an a
property (for instance)
It seems to work to base the loop on the keys of the target (foo
) and doing a check that the key exists in the source (bar
) while requiring that they share a common subtype via a generic parameter, like this:
function copyAllProps<ObjectType>(target: ObjectType, source: ObjectType) {
// The `as` below is a bit of a lie :-D
(Object.keys(target) as Array<keyof ObjectType>).forEach(k => {
// NOTE: The type of `k` here is slightly wrong. We told TypeScript
// it's `keyof ObjectType`, but it could be something else, because
// `target` can have keys that `source` doesn't have (`target`'s type can
// be a supertype of `source`'s type). This `if` mitigates that by
// checking that `source` does have the property.
if (k in source) {
target[k] = source[k];
}
});
}
(Note the caveats within.) That only copies the common properties.
Usage examples:
// Your original examples
let foo: IMixed = { a: 1, b: 'one', c: true };
let bar: IMixed = { a: 2, b: 'two', c: false };
let bar2 = { a: 2, b: false, c: 'two'};
let foo3 = { a: 1, b: 'one', c: true, d: new Date() };
let bar3 = { a: 2, b: 'two', c: false, d: null };
function test() {
copyAllProps(foo, bar); // <== Works
copyAllProps(foo, bar2); // <== Error as desired, same keys but different types for `b` and `c`
copyAllProps(foo3, bar3); // <== Error as desired, same keys but different types for `d`
copyAllProps(foo, {}); // <== Error as desired, source isn't a match
}