Home > Blockchain >  How to assert matching types when iterating over an object
How to assert matching types when iterating over an object

Time:10-19

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);

Playground

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];
});

Playground

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
}

Playground link

  • Related