Skip to content

Non-Distributive Omit<T,K> used by library breaks discriminated unions, solution included #421

@tommysullivan

Description

@tommysullivan

import { optic, optic_ } from "optics-ts";

//this demonstrates the problem with using optics to modify union types (whether using poly or monomorphic optics)

type Manual1 = {
  URI: "Manual1";
  p1: boolean;
  players: number[];
  reinforcementsCount: number;
};

type Manual2 = {
  URI: "Manual2";
  p2: number;
  players: number[];
  reinforcementsCount: number;
};

type OneOfTheStates = Manual1 | Manual2;
const opticsTest = (input: OneOfTheStates) => {
  //first some basic discriminated union sanity checks:
  //type "Manual1" | "Manual2" as expected
  const uri = input.URI;

  if (input.URI == "Manual1") {
    input.p1; //discriminated field only on Manual1 works here
  }

  //now a demo of the problem using the optics
  const reinforcementsCount_ = optic_<OneOfTheStates>().prop(
    "reinforcementsCount"
  );
  const reinforcementsCount = optic<OneOfTheStates>().prop(
    "reinforcementsCount"
  );

  //NOTE: Uncomment any of these 3 to get ts error:
  // const val_: OneOfTheStates = set(reinforcementsCount_)(3)(input);
  // const val: OneOfTheStates = set(reinforcementsCount)(3)(input);
  // return modify(reinforcementsCount)(a => 5)(input);
  
  //CULPRIT: Non-Distributive Omit<T,K> used by library breaks discriminated unions
  //Since i am using optics modify, the type of modify gives me an `Omit<Type, "propertyModified"> & { propertyModified: ItsType }` 
  //Omit here doesn't preserve the input type as a discriminated union; it creates a single type where URI is one or the other, and the shared properties are listed, 
  //but it loses the properties unique to each disrciminated union member.
  //because the implementation of Omit, turns out, is not distributive over unions.
  type X = Omit<OneOfTheStates, "reinforcementsCount">;
  const inputAsX = input as X;
  if ((inputAsX.URI == "Manual1")) {
    //Uncomment to see unexpected TS ERROR!
    // inputAsX.p1; 
  }

  //Since conditional types are distributive for unions (T is a union), we get a distribution of Omit<EachType, K> as a union result
  //preserving our ability to typecheck it:
  type DistributiveOmit<T, K extends keyof T> = T extends any
    ? Omit<T, K>
    : never;

  type X2 = DistributiveOmit<OneOfTheStates, "reinforcementsCount">;
    const inputAsX2 = input as X2;
    if (inputAsX2.URI == "Manual1") {
      inputAsX2.p1; //this works due to the solution of distributed omit
    }

SOLUTION: Use a distributive Omit in optics-ts

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions