Type 'A | B' is not assignable to type 'A & B'

Viewed 343

Why does this code not compile?

type A = { n: number }
type B = { s: string }
type Thing = {
  a: A
  b: B
}
function update(obj: Thing, path: keyof Thing) {
  obj[path] = obj[path]
}

I would expect both sides of the assignment to have type A | B but the TypeScript compiler fails with:

error TS2322: Type 'A | B' is not assignable to type 'A & B'.
  Type 'A' is not assignable to type 'A & B'.
    Property 's' is missing in type 'A' but required in type 'B'.

10     obj[path] = obj[path]
       ~~~~~~~~~

Is there a way to make it work?

4 Answers

Try this

function update<T extends Thing>(obj: T, path: keyof T) {
  obj[path] = obj[path]
}

When you access obj[path] it could either return an A or a B, and the compiler doesn't know which, and it (correctly) will not allow the assignment unless all constraints are satisfied:

type key = keyof Thing;
const keys: key[] = ['a', 'b'];

const path = keys[Math.round(Math.random())];
const thing: Thing = {
    a: {
        n: 3
    },

    b: {
        s: 'hi'
    }
};

const result = thing[path]; // A | B, compiler can't know which!

So now if I use path to access a Thing, do I get an A or a B? The compiler can't know, and it can't narrow the type at the point of access. You and I know that it's the same thing, but the compiler cannot prove it. Same goes for the funarg.

If you use something statically knowable, the compiler allows the assignment:

function update(obj: Thing) {
  obj.a = obj.a;
  obj['a'] = obj['a'];
}

Playground

So that covers the why, but in case you want to know how to get the compiler to understand the correct type, check out this canonical answer

This happens due to changes introduced in Typescript 3.5 to improve soundness of indexed access types.

When an indexed access T[K] occurs on the source side of a type relationship, it resolves to a union type of the properties selected by T[K], but when it occurs on the target side of a type relationship, it now resolves to an intersection type of the properties selected by T[K]. Previously, the target side would resolve to a union type as well, which is unsound.

type A = { n: number }
type B = { s: string }
type Thing = { 
  a: A
  b: B
}

declare const thing: Thing

const key = 'a' as keyof Thing

let t = thing[key] // t has type A | B
t = { s: '' }      // we can `safely` assign it a value of type `B`
 
thing[key] = t     // and here things break

playground link


For a working example refer to this answer.

You can use an index signature which accepts any key, with the value being an intersection of A & B.

type A = { n: number }
type B = { s: string }
type AB = A & B;
type Thing = {
  [key: string]: AB;
}
function update(obj: Thing, path: keyof Thing) {
  obj[path].n = 0;
  obj[path].s = "hello";
}
Related