Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript 3.7 Partial and is not assignable to type never/undefined

Tags:

typescript

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?

like image 599
piotrgajow Avatar asked Nov 07 '19 12:11

piotrgajow


Video Answer


2 Answers

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

like image 75
jcalz Avatar answered Sep 28 '22 16:09

jcalz


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#

like image 20
Mor Shemesh Avatar answered Sep 28 '22 15:09

Mor Shemesh