gvergnaud / ts-pattern

🎨 The exhaustive Pattern Matching library for TypeScript, with smart type inference.
MIT License
12.51k stars 135 forks source link

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

Closed thawankeane closed 1 year ago

thawankeane commented 1 year ago

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

    }
  )
darrylnoakes commented 1 year ago

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.

thawankeane commented 1 year ago

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?

darrylnoakes commented 1 year ago

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.

thawankeane commented 1 year ago

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