11

When I look at the signature of Object.assign I see that it uses intersection types, but the behavior is not what I expected.

I want to use Object.assign to merge some changes with an old object to create a new object (as is common practice in JS when following immutable data patterns) and have TypeScript validate that the parts and the result are correct, but it seems TypeScript allows anything through without an error.

For example:

interface Thing {
    name: string;
    age: number;
    fav: boolean;
}

let oldThing: Thing = {
    name: "Aaa",
    age: 123,
    fav: false
};

let newThing: Thing = Object.assign({}, oldThing, {
    name: "Bbb",
    age: "abc", // should be type error
    fake: "fakey" // should be unknown prop error
});

Here it is in the playground.

Why does Object.assign allow this incorrect assignment to Thing? Is there a way to make this work close to what I expected?

3
  • You've met all the requirements to make the typescript compiler happy, but remember at the end of the day it's javascript that runs your code and javascript just doesn't care. Commented Oct 6, 2016 at 21:48
  • Yep, I'm reminded of that all the time. :) But my question here is why does it compile? It seems the assignment to Thing should be a compile error on those properties. Maybe I don't understand how the type checker validates assignment. Commented Oct 6, 2016 at 21:58
  • It's been almost 3 years, still no progress with Object.assign. I'm kind of confused as to why there's so little information about this issue. I think it should be described in official docs. Commented Jul 21, 2019 at 8:22

2 Answers 2

8

You can force TypeScript to check using type assertions: i.e., as Thing or <Thing>

interface Thing {
    name: string;
    age: number;
    fav?: boolean;  // You may need to mark this as optional depending on
                    // your requirement, otherwise TypeScript will 
                    // generate missing prop error
}

//...

let newThing: Thing = Object.assign({}, oldThing, {
    name: "Bbb",
    age: "abc",     // will cause type error
    fake: "fakey"   // howerver, no error for unknown prop
} as Thing);
Sign up to request clarification or add additional context in comments.

2 Comments

This is the correct answer. Thanks! I did not know I could specify the object's type with <Thing>{ ... } syntax.
With newer reversion of Typescript (since version 4 I think) it's better to use "satisfies" than "as" because it just checks the type rather than casting.
7

The reason you intuitively expect an error here is that the intersection type constructed for the return value of Object.assign is absurd, and cannot be occupied by any real values. However, TypeScript has no way of verifying this. The return value from Object.assign here is typed as the following:

{
    name: string,
    age: string & number,
    fav: boolean,
    fake: string
}

Of course there is no actual value that is both string & number, but the compiler doesn't know that. It happily constructs this absurd type, then when you try to assign it to Thing verifies that the type of each property in Thing is satisfied by the corresponding property in the return value. Since this is the case (each of name, age and fav are present, and satisfy at least the required interface in Thing), the assignment succeeds.

If you allowed the type of newThing to be inferred, then tried assigning anything to the age property, you'd see the problem:

let newThing = Object.assign({}, oldThing, {
    name: "Bbb",
    age: "abc",
    fake: "fakey"
});
newThing.age = 10;   // Compiler error because 10 is not a string
newThing.age = "10"; // Compiler error because "10" is not a number

3 Comments

Thanks, that is funky. So I guess Object.assign doesn't really help enforce type-safety for things like this. Looking forward to TS supporting object spread instead...
It's probably a new development since 2016, but there's a name for string & number: never. The compiler knows it has no values, but it's fine to have an object with a field of type never so long as you don't try to assign to it. (The compiler will allow you to read from it and will implicitly convert it to any type, which sounds weird, but never is really meant for things like the return type of a function that always throws an error.)
@JohnWilliams Yes, never is the absurd type, and it does appear that string & number now evaluates to never. It's not clear to me that in general the compiler is capable of reducing an arbitrary intersection type to never when it has no inhabitants (given how sophisticated the type system is), but it'd be pretty cool if we have that property now.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.