Home > Enterprise >  Typescript Compiler Not Enforcing Generics
Typescript Compiler Not Enforcing Generics

Time:09-26

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

CodeSandbox Link Here

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.

Example

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

  • Related