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

Wrong type with when guard and generics

Open zoontek opened this issue 2 years ago • 6 comments

Hi 👋

As I try to create compatibility between this library and ts-belt, I noticed an issue with when guards when they use generics.

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

type Nullish = null | undefined;

const someGuard = <T>(option: T | Nullish): option is T => option != null;
const noneGuard = <T>(option: T | Nullish): option is Nullish => option == null;

const __Some = when(someGuard);
const __None = when(noneGuard);

const array = Array<string>(Math.floor(Math.random() * 10)).fill("foo");
const item = array[5];

if (someGuard(item)) {
  console.log(item); // type of item is string
}
if (noneGuard(item)) {
  console.log(item); // type of item is undefined
}

match(item)
  .with(__Some, item => { /* type of item is string | undefined, since ts-pattern struggles with generic type guards */ })
  .with(__None, item => { /* type of item is undefined */ })
  .exhaustive();

It works without generic:

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

type Nullish = null | undefined;

const someStringGuard = (option: string | Nullish): option is string => option != null;
const noneStringGuard = (option: string | Nullish): option is Nullish => option == null;

const __Some = when(someStringGuard);
const __None = when(noneStringGuard);

const array = Array<string>(Math.floor(Math.random() * 10)).fill("foo");
const item = array[5];

if (someGuard(item)) {
  console.log(item); // type of item is string
}
if (noneGuard(item)) {
  console.log(item); // type of item is undefined
}

match(item)
  .with(__Some, item => { /* type of item is string */})
  .with(__None, item => { /* type of item is undefined */ })
  .exhaustive();

Is there a way to achieve this (a generic guard pattern)?

zoontek avatar Dec 14 '21 12:12 zoontek

TS-Pattern doesn't support generic type guards, but you can achieve what you want to do in your example with a not(__.nullish) pattern.

The issue with generic types is there is no way (AFAIK) to "propagate" the type parameter to the outside of your guard function, or to pass a generic type and instantiate it with an arbitrary type later. Here is an example to show you what I mean:


const someGuard = <T>(option: T | Nullish): option is T => option != null;

// Let's pretend this is what `when(someGuard)` returns
const whenPattern = {
  type: 'when',
  predicate: someGuard
} 

// The function `with` has  access to the `Input` and `Output` types of the match expression
function with(pattern: typeof whenPattern, (value: InferValue<Input, typeof pattern>) => Output): Match<Input, Output>;                                                                             

type InferValue<Input, Pattern> =
     // I would need to instantiate our generic function with the type of input, but 
     // typescript doesn't have higher kinded types so I can't "call" a generic that would
     // be passed in type parameters like that :(
     Pattern['predicate']<Input> 
     //                  ^ This is invalid TS syntax
         extends (value: any) => value is infer Narrowed ? Narrowed : never

I think ts-toolbelt goes around this issue by providing a "fake" generic Any/x type that you can use in a type level expression and the library will interpolate it with the real type parameter later, so that you can do stuff like:

import {x} from 'Any/x'

type ToPairs<List extends any[]> = Map<List, [x, x]>

type x = ToPairs<[1,2,3]>
// => [[1, 1], [2, 2], [3, 3]]

Maybe I could do something similar to support generics, but I'm not sure if that would work.

gvergnaud avatar Dec 21 '21 10:12 gvergnaud

That was I though 😞

Unfortunately, I simplified the Option type for the example, but ts-belt use an opaque type for Option, so it's not possible to simply use:

const __Some = __.nullish;
const __None = not(__.nullish);

zoontek avatar Dec 21 '21 12:12 zoontek

Looks like my wish is about to come true 🎉 https://github.com/microsoft/TypeScript/pull/47607

With this PR, doing Pattern['predicate']<Input> will become possible, so supporting generic guards should be doable!

gvergnaud avatar Feb 04 '22 22:02 gvergnaud

@gvergnaud The PR is now merged. any way to support generics now ?

Armaldio avatar Jul 21 '22 12:07 Armaldio

eagerly anticipating this change!

nlundquist avatar Oct 08 '22 23:10 nlundquist

I might have been overly optimistic... 😅 Looks like this type-level function application syntax doesn't work on type parameters, so what I had in mind is still not possible...

declare function genericFn<T> (v: T): [T,T]

// works
type T1 = ReturnType<typeof genericFn<number>>
//    ^? [number, number]

type Fn1<T extends <A>(a: A) => any> = ReturnType<T<number>>
//                                                ^ type error

type T2 = Fn1<typeof genericFn>
//    ^? any

Playground

gvergnaud avatar Oct 24 '22 11:10 gvergnaud