After upgrading typescript version to 3.7 I get type errors in 2 functions. The functions work correctly (when adding @ts-ignore everything is fine).
interface A {
x: string,
y: number,
}
interface B {
x: string,
y: number,
z: boolean,
}
function extract(input: B, keys: Array<keyof A>): Partial<A> {
const extract: Partial<A> = {};
keys.forEach((key: keyof A) => {
extract[key] = input[key]; // error!
// ~~~~~~~~~~~~ <-- 'string | number' is not assignable to 'undefined'
});
return extract;
}
function assign(target: B, source: Partial<A>): void {
(Object.keys(source) as Array<keyof A>).forEach((key) => {
target[key] = source[key]!; // error!
// ~~~~~~~~~~~ <-- 'string | number' is not assignable to type 'never'
});
}
const test: B = { x: "x", y: 1, z: true };
console.log(extract(test, ["y"])); // -> { y: 1 }
assign(test, { x: "new" });
console.log(test); // -> { x: "new", y: 1, z: true }
The code along with errors can be found at ts playground
Is there any way to implement this the right way without @ts-ignore?
This is a known breaking change introduced in TypeScript 3.5 to prevent unsound writes to indexed access types. It has had some good effects by catching actual bugs, and it's had some unfortunate effects by falsely warning on perfectly safe assignments, as you can see.
The simplest way to get around this is to use a type assertion:
(extract as any)[key] = input[key];
(target as any)[key] = source[key];
There are safer assertions than any
, but they are more complicated to express.
If you want to avoid type assertions, you'll need to use some workarounds. For extract()
, it suffices to use a generic callback function inside forEach()
. The compiler sees the assignment as being both from and to a value of the identical generic type Partial<A>[K]
, which it allows:
function extract(input: B, keys: Array<keyof A>): Partial<A> {
const extract: Partial<A> = {};
keys.forEach(<K extends keyof A>(key: K) => {
extract[key] = input[key];
});
return extract;
}
For assign()
that target[key] = source[key]
won't work even with a generic key
of type K
. You're reading from a generic type NonNullable<Partial<A>[K]>
and writing to a different generic type B[K]
. (I mean "different" in the sense that the compiler doesn't represent them identically; of course they are the same type when you evaluate them.) We can get back the identical type by widening the target
variable to Partial<A>
(which is fine because every B
is also a Partial<A>
, if you squint and don't think about mutations).
So I'd do it like this:
function assign(target: B, source: Partial<A>): void {
const keys = Object.keys(source) as Array<keyof A>;
const widerTarget: Partial<A> = target;
keys.forEach(<K extends keyof A>(key: K) => {
if (typeof source[key] !== "undefined") { // need this check
widerTarget[key] = source[key];
}
});
}
Oh and I added that undefined
check because assign(test, { x: "new", y: undefined })
is allowed; the language doesn't really distinguish missing from undefined
.
Anyway, those will work as desired. Personally I'd probably just use a type assertion and move on.
Okay, hope that helps; good luck!
Link to code
I found the following solution using a helper function:
interface A {
x: string,
y: number,
}
interface B {
x: string,
y: number,
z: boolean,
}
// Using this method, is better accepted by the transpiler
function typedAssign<T>(source: T, target: Partial<T>, key: keyof T, force: boolean = false) {
if (force) {
target[key] = source[key]!;
} else {
target[key] = source[key];
}
}
function extract(input: B, keys: Array<keyof A>): Partial<A> {
const extract: Partial<Pick<B, keyof A>> = {};
keys.forEach((key: keyof A) => {
typedAssign(input, extract, key);
});
return extract;
}
function assign(target: B, source: Partial<A>): void {
(Object.keys(source) as Array<keyof A>).forEach((key) => {
typedAssign(source, target, key, true);
});
}
const test: B = { x: "x", y: 1, z: true };
console.log(extract(test, ["y"])); // -> { y: 1 }
assign(test, { x: "new" });
console.log(test); // -> { x: "new", y: 1, z: true }
TS Playground: http://www.typescriptlang.org/play/?ssl=1&ssc=1&pln=37&pc=53#
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