Skip to content

Performance and usablity of .exhaustive #16

@m-rutter

Description

@m-rutter

So I noticed that exhaustive has some pretty punishing compile times for any moderately complicated types it needs to match on. I've even found it hitting the "union type that is too complex to represent" limit regularly. I don't have any benchmarks. but I think you are aware of the problem as you mention it your docs. I'm not sure if exhaustive is actually usable except for the most simple cases.

If I had to guess it is because if you have a type like this:

type A = {type: "a", mode: "b" | "c" | "d"} |  {type: "b", mode: "f" | "g"}

You have to generate a union that looks like this?:

  | {type: "a", mode: "b"} 
  | {type: "a", mode: "c"} 
  | {type: "a", mode: "d"} 
  | {type: "b", mode: "f"} 
  | {type: "b", mode: "g"}   

So for example this fairly simple to understand union will completely break exhaustive:

import { Property } from "csstype";

declare const a:
  | { type: "textWithColor"; color: Property.Color }
  | { type: "textWithColorAndBackground"; color: Property.Color; backgroundColor: Property.Color };

match2(a).exhaustive(); // "union type that is too complex to represent"

playground link - takes serveral minutes on my machine to hit the limit

The reason being is that Property.Color is a string union with hundreds of variants. (this is the same kind of example that eventually lead the typescript team to abandon by default inference of template string literals types in 4.2 - microsoft/TypeScript#42416)

So this makes exhaustive pretty much unusable if you have a type with properties that are unions of any moderate size. Either because you will hit the union limit or because the compile times are too extreme to make it practical to use.

This is all fair, and I don't think I see a way around the issue and keeping the full pattern matching features of the lib.

That said, in 80-90% of cases all I want to match on is the discriminator of a union in order to narrow the types. For example this works in a simple switch and I still get some kind of exhaustiveness checks:

declare const a:
  | { type: "textWithColor"; color: Property.Color }
  | { type: "textWithColorAndBackground"; color: Property.Color; backgroundColor: Property.Color };
  
  
 const aToDescription (a: typeof a): string  => { 
   switch(a.type) {
    case "textWithColor":
       return a.color
     case "textWithColorAndBackground":
       return `${a.color} with a background of ${a.backgroundColor}`
   }
}

I'm wondering if you can offer something that still allows for some kind of exhaustiveness checks on discriminators in order to narrow types down, but compromises on pattern matching features elsewhere for the sake of compile time performance. In the back of my mind I'm thinking about this lib https://paarthenon.github.io/variant/ (mentioned in my my previous issue) because this library can do exhaustiveness checks because it only concerns itself with the discriminators of unions.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions