ts-pattern
ts-pattern copied to clipboard
Wrong type with when guard and generics
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)?
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.
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);
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 The PR is now merged. any way to support generics now ?
eagerly anticipating this change!
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