ts-pattern icon indicating copy to clipboard operation
ts-pattern copied to clipboard

Select produces wrong types

Open DeluxeOwl opened this issue 2 years ago • 3 comments

Hi, I'm trying to extract useState patterns from react, but the selections object has the wrong types (some properties aren't even added): image

import { match, P } from "ts-pattern";

const isObject = (value: unknown): value is Object =>
  Boolean(value && typeof value === "object");

const useStatePattern = {
  type: "VariableDeclaration",
  kind: "const",
  declarations: [
    {
      type: "VariableDeclarator",
      id: {
        type: "ArrayPattern",
        elements: [
          {
            type: "Identifier",
            name: P.select("stateIdentifier", P.string),
          },
          {
            type: "Identifier",
            name: P.select("setStateIdentifier", P.string),
          },
        ],
      },
      init: {
        type: "CallExpression",
        callee: {
          type: "Identifier",
          name: "useState",
        },
        // this matches the exact length
        arguments: [P.select("value", P.when(isObject))],
        typeParameters: P.optional(
          P.select("typeParameters", P.when(isObject))
        ),
      },
    },
  ],
};

// here when hovering over selections
match<any, any>({})
  .with(useStatePattern, (selections) => selections)
  .otherwise(() => null) as any;

Using the latest version as of today "ts-pattern": "^5.0.1"

DeluxeOwl avatar Jul 10 '23 12:07 DeluxeOwl

Hey,

First, here is a workaround: you can use as const after your pattern declaration, and TS-Pattern will work as expected: Playround

const useStatePattern = { ... } as const;

match<any, any>({})
      .with(useStatePattern, (selections) => 
      /* selections: {
            value: Object;
            stateIdentifier: string;
            setStateIdentifier: string;
            typeParameters: Object | undefined;
        }  */
      )
      .otherwise(() => null);

It doesn't work without it because TypeScript will infer the type of arrays contained inside useStatePattern as arrays of unions rather than tuple types by default, e.g. (A | B)[] instead of [A, B]. TS-Pattern ignores inline arrays because they aren't precise enough to perform exhaustive checking, or to really know what it's inside each index and what has been selected (when the type of input is known ahead of time). This wouldn't happen if your pattern was defined inline, inside the with clause:

match<any, any>({})
  .with({  /* ... your useStatePattern inlined */  }, (selections) => selections) // would work
  .otherwise(() => null) as any;

That said, I think we should change the Selections type to infer what it can from an array, even though the type of the selected value could be a little bit imprecise.

gvergnaud avatar Jul 12 '23 10:07 gvergnaud

Thanks for the answer, the workaround works fine for my use case

DeluxeOwl avatar Jul 12 '23 11:07 DeluxeOwl

It turns out that part of this bug is due to what I believe is a bug in the type checker when deduplicating arrays elements that use phantom type parameters: Playground

I'm planning to open an issue on TypeScript's repository, keeping this issue open in the meantime.

gvergnaud avatar Jul 12 '23 11:07 gvergnaud