gvergnaud / ts-pattern

🎨 The exhaustive Pattern Matching library for TypeScript, with smart type inference.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Select produces wrong types

DeluxeOwl opened this issue · 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"

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.

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

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.