Question
I have a reducer model with an action that accepts a payload
expecting a shape of { id: string, newValues: Partial<A_or_B> }
. In the code below, the enforcement of type A_or_B
is not enforced when I declare newValues
inline; however, if I declare newValues
ahead of time and assign properties to it, the constraints are enforced. How can I enforce the constraints on A_or_B
even when newValues
is declared inline?
Problematic Code
import { useContext } from "react";
import { AppDispatchContext } from "../src/AppProvider";
import { ActionTypes, A_or_B } from "./interfaces";
export default function App() {
const dispatch = useContext(AppDispatchContext);
const x: Partial<A_or_B> = {};
// Property 'color' does not exist on type
// 'Partial<A> | Partial<B>'.
// Property 'color' does not exist on type 'Partial<A>'.
x.color = "red";
dispatch({
type: ActionTypes.POINT_UPDATED,
// why can I assign a color to Partial<A>|Partial<B> if 'color'
// only exists on B?
payload: { id: "x", newValues: { color: "red" } }
});
}
CodeSandbox Link
CodePudding user response:
The constraints actually are being enforced in your payload. You can verify this by adding a property on your payload that doesn't exist on type A or B.
The real issue here is Type Inference.
const x: Partial<A_or_B> = {};
// Property 'color' does not exist on type
// 'Partial<A> | Partial<B>'.
// Property 'color' does not exist on type 'Partial<A>'.
x.color = "red";
x
is assigned a valid union value but Typescript can't tell if it's of type A or B, so x.color
can't be verified since TS thinks there is a possiblity it can be of type B.
payload: { id: "x", newValues: { color: "red" } }
newValues
is explictly set to an object that only contains color
which means Typescript knows that this must be of type A.
CodePudding user response:
This is a common problem most people experience with union
types in typescript. Some treat them as if they were intersection types, while a few wish they behaved more like mutually exclusive types.
However, typescript has two different interpretations of union types, depending on your use-case.
Properties
When you attempt to access the property of a union type, you can only access properties that are common amongst all members of the union. Another way to say this is that you only have access to the intersection of the properties of the members of the union.
That may be a mouthful, but the typescript docs give a nice analogy to explain this:
For example, if we had a room of tall people wearing hats, and another room of Spanish speakers wearing hats, after combining those rooms, the only thing we know about every person is that they must be wearing a hat.
So in the case of property access, typescript only allows a subset of properties to be accessible (without any further hints from the user), thus forming an intersection.
Membership (aka type inference)
When it comes to type inference, the compiler is quite liberal with what it allows to match a union type. Essentially it says, if you want to be part of this group, you just have to look like one of the members of this group. In this case, you don't have to have a property that matches the property of every member of the union.
Maybe the solution to your problem is to just use intersection type for x
:
const x: Partial<A&B> = {};
x.color = "red"; // works!
...
newValues: x; // also works
This solves the problem of both property access and membership