monet / monet.js

monet.js - Monadic types library for JavaScript
https://monet.github.io/monet.js/
MIT License
1.6k stars 114 forks source link

Maybe.filter should work as type guard #235

Closed tvedtorama closed 3 years ago

tvedtorama commented 3 years ago

I have a quite frequent problem, where I make an assertion in filter which determines type. For the following map I expect that the type to be adapted based on the new information. It is not, the output type T is the same as in the input in filter.

Example:

const value: {x: "A", value: number} | {x: "B", noValueHere: string} = Math.random() < 0.5 ?
    {x: "A", value: 10} :
    {x: "B", noValueHere: "hello"}
const result = Some(value)
    .filter(({x}) => x === "A")
    .map(item => item.value)
    .orSome(-1)

This errors on the map following the filter.

A workaround:

const value: {x: "A", value: number} | {x: "B", noValueHere: string} = Math.random() < 0.5 ?
    {x: "A", value: 10} :
    {x: "B", noValueHere: "hello"}
const result = Some(value)
    .flatMap(item => Maybe.fromUndefined(item.x === "A" ? item : undefined))
    .map(item => item.value)
    .orSome(-1)

I don't know if this is easy/possible to fix, but it would be very nice.

tvedtorama commented 3 years ago

Not sure if it helps, but here is the definition of filter from ixjs, which supports type guards:

export declare function filter<T, S extends T>(predicate: (value: T, index: number) => value is S, thisArg?: any): OperatorFunction<T, S>;
ulfryk commented 3 years ago

@ulfryk sorry for slight delay. I think we can update filter typing to help with that.

Also you can try creating a type checking funcion as a workaround for now:

const isAType = (v: {x: "A", value: number} | {x: "B", noValueHere: string}): v is {x: "A", value: number} => v.x === "A";

and then below should work (didn't check for 100%):

const result = Some(value)
    .filter(isAType)
    .map(item => item.value)
    .orSome(-1)
tvedtorama commented 3 years ago

@ulfryk You would need the function anyways, my original sample - with just a check, no function - would probably not work in any circumstances. But I don't think the current implementation will cut it. I modified your example slightly:

type T = {x: "A", value: number} | {x: "B", noValueHere: string}
const isAType = (v: T): v is {x: "A", value: number} => v.x === "A";
const value: T = null // Or anything, but if concrete - TS fools us into believing this works

const result = Some(value)
    .filter(isAType)
    .map(item => item.value) // Error 
    .orSome(-1)

The reason being that filter returns a Monad of the same type, no option to modify it:

filter(fn: (val: T) => boolean): Maybe<T>;
ulfryk commented 3 years ago

@tvedtorama you are completely right. Do you think you can create PR with a fix for filter typings ?

tvedtorama commented 3 years ago

Maybe - I'll put it on my list. This is a problem I run into now and then - so it would make sense.

mlrv commented 3 years ago

I took some time to work on this and opened #240, let me know if something like this could work for your use case @tvedtorama

ulfryk commented 3 years ago

@tvedtorama can you check if nightly 0.9.1-466 works as you need ?

tvedtorama commented 3 years ago

I'm not sure if I got the skills or time to check this right now, but I looked at the code/tests - and it looks perfect!

ulfryk commented 3 years ago

OK works --> https://stackblitz.com/edit/typescript-xyxmhl

I will clean things up and release 0.9.2 this week

ulfryk commented 3 years ago
A new version of the package monet (0.9.2) was published at 2021-03-05T17:36:17.909Z from
35.227.97.188. The shasum of this package was c59d4390937a1b021126844b50b993963c6b0ec7.