gvergnaud/ts-pattern

Support type predicate return type at `.when()` callback

thawankeane opened this issue · 4 comments

When working with type predicates, the function .when() will always accept an type (value: TInput) => TOutput as argument, but it will be very helpful (if possible) to infer the value as the return type of predicate argument, let me show some examples:

Imagine that will have the following classes

class Fish {
  public swim() {}
}

class Bird {
  public fly() {}
}

class Animal {
  public speed: number;
  
  public isFish(): this is Fish {
    return !!(this as Fish).swim;
  }
  
  public isBird(): this is Bird {
    return !!(this as Bird).fly;
  }
}

Now, you want to use ts-pattern to exaustive check an Animal

import { match } from "ts-pattern";

const unknownAnimal: Animal = {
  // implementation
};

match(unknownAnimal)
  .when(
    (animal) => animal.isFish(), // right here the `animal` argument is corrected inferred as an `Animal` as expected
    (fish) => {  // here, the `fish` argument isn't inferred as `Fish`, instead it's also inferred as `Animal`, what would cause inconsistent types
      fish.swim() // ts error cause `swim` does not exists on type `Animal`
    }
  )
  // other checks

Describe the solution you'd like
I would like if the second argument (.when callback) was typed as the return type of the first argument if possible

Maybe I might be missing something, so feel free to correct me if I'm doing something wrong

Describe alternatives you've considered
I tried to explicit type the callback, but typescript doesn't allow to convert our input to the inferred type guard

Example:

.when(
    (animal) => animal.isFish(), 
    (fish: Fish) => {  // explicit type `Fish`, ts throw an ts(2345) error

    }
  )

For clarity: You don't want the handler's input narrowed to the return type of the predicate, you want the predicate to act as a type guard.

The main issue is knowing whether the predicate function even is a type guard. The only sensible solution is to explicitly annotate it.

Then it's a matter of typing .when() to make use of that, if possible.

For clarity: You don't want the handler's input narrowed to the return type of the predicate, you want the predicate to act as a type guard.

Yeah, you're right

The main issue is knowing whether the predicate function even is a type guard. The only sensible solution is to explicitly annotate it.

As I'm using classes based typings, I ended up using .with in combination with P.instanceOf, but in a functional approach explicitly annotate would be a solution, could you please share some examples of how these annotations would be?

Taking your example:

match(unknownAnimal)
  .when(
    (animal): animal is Fish => animal.isFish(),
    (fish) => {
      fish.swim()
    }
  )

But I'm not sure if TS Pattern uses that to narrow the handler input.

But I'm not sure if TS Pattern uses that to narrow the handler input.

Yeah, it does. Thank you @darrylnoakes

https://tsplay.dev/N9Yz1N