Closed SimonMeskens closed 6 years ago
If you add your Either, I can show you how to do the same for that one. I'm currently working on improving the hover types in the IDE, so they don't look like an explosion of random stuff.
@SimonMeskens The Either
is here: https://gist.github.com/masaeedu/5290d0bff128897a59e985f191c102a7#file-index-js-L18. It actually turns out that the instance for Maybe
and Either
is totally isomorphic, so you can use the same one for both.
It's too mindboggling to think about the types though, so maybe you want const GenericMaybe: TGenericMaybe = GE; const GenericEither: TGenericEither = GE;
.
Simplified code a bit, should get way better intellisense now
Seems like that broke something too, I'll look at it tomorrow
Nevermind, was an easy fix. Should be all better now. Intellisense is better, but still ugh. I'll have to figure out a small hack to trick it into using an interface, but that's not always easy or possible.
Question: is the typing you wrote for match correct? You have this:
// match :: { "Just" :: a -> b, "None" :: b } -> T a -> b
This indicates "Just"
is a mapping operation from a -> b
, but then in chain
, you give it a chain operation instead:
const chain = f => match({ None, Just: f });
Since chain
only has access to the chain operation, I'll assume you meant:
// match :: { "Just" :: a -> T b, "None" :: b } -> T a -> b
Also, am I allowed to release the eventual Maybe and Either code as examples on this repo? I'm preparing to release an alpha version as soon as the bugs are worked out (there's quite a few left).
new version. I think this one is exactly what you wanted to write in the first place. Just took me a while to work through and understand the code correctly.
Can you verify this one is exactly what you wanted @masaeedu?
I'm going to start work on the Either example now
import { Generic } from "typeprops";
interface GenericMaybe<F> {
of: <T>(x: T) => Generic<F, [T]>;
map: <T, U>(f: (a: T) => U) => (o: Generic<F, [T]>) => Generic<F, [U]>;
chain: <T, U>(
f: (a: T) => Generic<F, [U]>
) => (o: Generic<F, [T]>) => Generic<F, [U]>;
}
interface GenericMatch<F, None> {
<T, U>(
_: {
Just: (x: T) => Generic<F, [U]>;
None: None;
}
): (o: Generic<F, [T]>) => Generic<F, [U]>;
}
// Just :: a -> T a
// None :: T a
// match :: { "Map" :: a -> T b, "None" :: b } -> T a -> b
const GenericMaybe = <
TJust extends <T>(a: T) => Generic<TMaybe, [T]>,
TNone,
TMaybe = ReturnType<TJust> | TNone
>({
Just,
None,
match
}: {
Just: TJust;
None: TNone;
match: GenericMatch<TMaybe, TNone>;
}): GenericMaybe<TMaybe> => {
// Functor
// map :: (a -> b) -> T a -> T b
const map = <T, U>(f: (a: T) => U) =>
match({ None, Just: (x: T) => Just(f(x)) });
// Monad
// of :: a -> T a
const of = Just;
// chain :: (a -> T b) -> T a -> T b
const chain = <T, U>(f: (a: T) => Generic<TMaybe, [U]>) =>
match({ None, Just: f });
return { map, of, chain };
};
// A possible representation of a Maybe
const k = Symbol("my maybe key");
type Maybe<T> = Just<T> | None;
type Just<T> = { [k]: T };
type None = {};
declare module "typeprops" {
interface TypeProps<Type, Params extends Tuple = never> {
["maybe"]: {
parameters: Type extends Maybe<infer A> ? [A] : never;
type: Type extends Maybe<any> ? Maybe<Params[0]> : never;
};
}
}
const Just = <T>(x: T) => ({ [k]: x } as Maybe<T>);
const None = {};
const match = <T, U>({ Just, None }: { Just: (x: T) => Just<U>; None }) => (
o: Maybe<T>
): Maybe<U> => (o.hasOwnProperty(k) ? Just(o[k]!) : None);
const { map, chain, of } = GenericMaybe({ Just, None, match });
// Examples
console.log([
map((x: number) => x + 2)(of(42)),
map((x: number) => x + 2)(None),
chain((x: number) => (x > 40 ? None : of(x * 2)) as Maybe<number>)(of(42)),
chain((x: number) => (x > 40 ? None : of(x * 2)) as Maybe<number>)(of(32)),
map((x: number) => None)(of(42)),
map((x: number) => of(x * 2))(of(42))
]);
Since chain only has access to the chain operation, I'll assume you meant:
@SimonMeskens No, it really is match :: { "Just" :: a -> b, "None" :: b } -> T a -> b
. The Just
case is allowed to produce anything, a number/string/potato or even another Maybe
, and that's what's going to be returned, without anything being wrapped in a Maybe
. Think of match
as a way to "fold" the data structure (in whatever case it may be) to an arbitrary value.
Also, am I allowed to release the eventual Maybe and Either code as examples on this repo?
Be my guest; a link to the gist would be appreciated, but is not necessary.
Regarding const chain = f => match({ None, Just: f })
, here's the type signatures of chain
and match
side by side:
// chain :: (a -> T b) -> T a -> T b
// match :: { "Just" :: c -> d, "None" :: d } -> T c -> d
Instantiate match
with c -> d
set to a -> T b
and d
set to T b
(since we're passing Just: f
and None: None
) and you'll get match({ None, Just: f }) :: Ta -> T b
. So overall the expression f => match({ None, Just: f })
has the correct type signature: (a -> T b) -> T a -> T b
.
Alright, I'll fix that, I understand what you mean. I also just noticed that interface GenericMaybe<F>
is just the monad spec, etc. I'll finish the examples and put them in the repository, with proper attribution. Are you fine with Apache 2.0 license for your code? I think it's compatible with MIT and ISC, but I switched to it recently because some of my things will require a patent clause and that one seems to protect me more for what it's worth.
Are you fine with Apache 2.0 license for your code?
@SimonMeskens Sure, although I myself am much more partial to the wtfpl. :)
Yeah, I'm basically wtfpl, I don't care about attribution and generally, everything I do is considered to be public domain. The only reason I switched to Apache, is because I have seen some of my ideas from years ago being used to make a lot of money, which I don't care about, but I do care no one gets a patent on anything I come up with. Not that that's ever going to come up, but hey, I do some stuff in national politics, so you never know.
EDIT: fixed a little hickup in the code
After much testing, I found a generic Match
type, it's indeed possible to write one, but due to a bug that might never get fixed in TypeScript, it's currently broken. Not that big of a deal, but it's nifty code nonetheless, so I'll share it. Note that it doesn't really rely on typeprops
that much, except to grab the type parameter of the generic type you pass in.
type Match<TGeneric, TConsKeys extends string> = {
<TCons extends { [Key in TConsKeys]: any }>(constructors: TCons): <
T,
U = {
[Key in keyof TCons]: TCons[Key] extends (a: T) => infer U
? U
: TCons[Key]
}[keyof TCons]
>(
generic: Generic<TGeneric, { "0": T; length: number }>
) => U;
};
const maybe = {
just: <T>(value: T) => ({ value } as Just<T>),
none: {} as None
};
const a: Match<Maybe, keyof typeof maybe> = null as any;
const b = a(maybe);
const c = b({ value: 4 });
The problem is that due to Microsoft/TypeScript#22617, it can't infer the return type of maybe.just
, since the output relies on the input type. I assume I'll run into some trouble with the other samples too, but hopefully, I can work around it for specific cases.
Alright, this should be correct now?
// Based on provided sample by Asad Saeeduddin
import { Generic } from "typeprops";
import { Monad } from "./types";
// just :: a -> T a
// none :: T a
interface GenericMaybeDefinition<
TJust extends <T>(value: T) => Generic<TMaybe, [T]>,
TNone extends Generic<TMaybe>,
TMaybe = ReturnType<TJust> | TNone
> {
just: TJust;
none: TNone;
match: GenericMaybeMatch<TMaybe>;
}
// match :: { "just" :: a -> b, "none" :: b } -> T a -> b
interface GenericMaybeMatch<TMaybe> {
<T, B, C>(
_: {
just: (value: T) => B;
none: C;
}
): (maybe: Generic<TMaybe, [T]>) => B | C;
}
const GenericMaybe = <
TJust extends <T>(value: T) => Generic<TMaybe, [T]>,
TNone extends Generic<TMaybe, any>,
TMaybe = ReturnType<TJust> | TNone
>({
just,
none,
match
}: GenericMaybeDefinition<TJust, TNone>): Monad<TMaybe> => {
// Functor
// map :: (a -> b) -> T a -> T b
const map = <T, U>(
transform: (value: T) => U
): ((maybe: Generic<TMaybe, [T]>) => Generic<TMaybe, [U]>) =>
match({ none, just: (value: T) => just(transform(value)) });
// Monad
// of :: a -> T a
const of = just;
// chain :: (a -> T b) -> T a -> T b
const chain = <T, U>(
transform: (value: T) => Generic<TMaybe, [U]>
): ((maybe: Generic<TMaybe, [T]>) => Generic<TMaybe, [U]>) =>
match({ none, just: transform });
return { map, of, chain };
};
// A concrete representation of a Maybe
type Maybe<T = any> = Just<T> | None;
type Just<T> = { value: T };
type None = { [key: string]: never };
declare module "typeprops" {
interface TypeProps<Type, Params extends ArrayLike<any>> {
["examples/maybe#maybe"]: {
parameters: Type extends Maybe<infer A> ? [A] : never;
type: Type extends Maybe ? Maybe<Params[0]> : never;
};
}
}
{
const just = <T>(value: T) => ({ value });
const none = {} as None;
const match = <T, B, C>({ just, none }: { just: (x: T) => B; none: C }) => (
maybe: Maybe<T>
): B | C => ("value" in maybe ? just((maybe as Just<T>).value) : none);
const { map, chain, of } = GenericMaybe({ just, none, match });
// Examples
console.log([
map((x: number) => x + 2)(of(42)),
map((x: number) => x + 2)(none),
chain((x: number) => (x > 40 ? none : of(x * 2)))(of(42)),
chain((x: number) => (x > 40 ? none : of(x * 2)))(of(32)),
map((x: number) => none)(of(42)),
map((x: number) => of(x * 2))(of(42))
]);
}
And here's the current definition of Monad, pretty simple:
interface Monad<TGeneric> {
// Functor
// map :: (a -> b) -> T a -> T b
map: <T, U>(
transform: (value: T) => U
) => (o: Generic<TGeneric, [T]>) => Generic<TGeneric, [U]>;
// Monad
// of :: a -> T a
of: <T>(value: T) => Generic<TGeneric, [T]>;
// chain :: (a -> T b) -> T a -> T b
chain: <T, U>(
transform: (a: T) => Generic<TGeneric, [U]>
) => (maybe: Generic<TGeneric, [T]>) => Generic<TGeneric, [U]>;
}
Some notes about Monad and Either: I think I'm fully able to type Either and I'm slowly working my way towards that, but one big problem in TypeProps right now is that I can't describe the Either Monad. The reason being that Either is not a monad, (Either T)
is a monad. To support some construct like that in TypeProps, I need some extra typing code and I think some of it is currently being blocked by some issues in TypeScript. That said, I'll start typing Either without abstracting its monadic interface and I'll put some code in that gives an error if you try to make a Generic with one parameter for a type that expects 2 or more or vice versa. I've also mapped out some useful constructs for later down the line that will make the library easier to use.
@SimonMeskens Could you perhaps represent it if you just skipped typechecking the left case? i.e. Either<number>
actually represents Either<{}, number>
?
I'm not really able to follow everything well enough to be able to make any actual contribution to the type stuff, but I was trying to prettify things a bit and came up with the following formulation:
type TGenericMaybe = ...
const GenericMaybe: TGenericMaybe = ({ just, none, match }) => {
// Functor
// map :: (a -> b) -> T a -> T b
type TMap =
<T, U>(transform: (value: T) => U)
=> (maybe: Generic<TMaybe, [T]>)
=> Generic<TMaybe, [U]>
const map: TMap = f =>
match({ none, just: (value: T) => just(f(value)) });
// ...
I think this separates the type stuff from the actual implementation a bit and might make things easier to read.
Unfortunately, you have to write out the type in full and can't put it above like that. Your example sorta works, but it secretly relies on f
being implicitly typed as any, which is not type-safe.
As for the Either case, the closest I can do (and I did that this morning and it works), is that if you give 1 type parameter to a 2 parameter type (or 4 types to a 7 parameter type, whatever), it always replaces the rightmost ones. I think I've seen very few types in Haskell where things are monadic over the first parameter instead of the second.
Doing that, I have made a general Monad that does work with multiple parameters (provided the type is monadic over the last parameter). It relies on some typelevel arithmetic, but that should be fine, I condensed it into a fairly readable format (if a bit verbose). I'll try to show it off tomorrow or so.
The issue is that TypeScript is unable to distinguish between (Maybe a) b and Maybe a b, because it basically duck-types the structure of the type. The only other option would be for me to "tag" generics somehow in a way that is invisible to the user, but one of the guidelines I started out with is that I want to try to stay away from "tags" and that the library should be able to type existing types, instead of the user having to shoehorn types to work with a HKT library.
I'll keep searching for a proper solution though.
I changed the definition of the concrete Maybe to:
const MAYBE = Symbol("maybe");
type Maybe<T = any> = Just<T> | None;
type Just<T> = { [MAYBE]: T };
type None = typeof MAYBE & { [key: string]: never };
I realized that it's a bad idea to put such an extremely simple type in the type dictionary, so I moved back to your original definition and made None a tagged type, which TypeProps was made to support from the start.
I think I'm now 100% happy with the typing for Maybe. I could do even better if the above-mentioned issue was ever fixed, so we can work on type constructors instead of the type they create, but that's not something that will come soon I'm afraid. This also means I uploaded the example to the repo. You can find the code in:
https://github.com/SimonMeskens/TypeProps/tree/master/examples
I also added the sample .js + the playground link in the post up-top for you to play around with. I'll finish the Either example next.
Either is proving harder to type than I thought. I'll be a while.
Maybe this can help: TypeScript Playground%3A%20Generic%3CF%2C%20B%3E%3B%0D%0A%7D%0D%0A%0D%0Ainterface%20Monad%3CF%20extends%20RegisteredTypes%2C%20A%3E%20extends%20Functor%3CF%2C%20A%3E%20%7B%0D%0A%20%20chain%3CB%3E(f%3A%20(_%3A%20A)%20%3D%3E%20Generic%3CF%2C%20B%3E)%3A%20Generic%3CF%2C%20B%3E%3B%0D%0A%7D%0D%0A%0D%0A%2F%20Identity%20%2F%0D%0Aclass%20Identity%3CA%20%3D%20any%3E%20implements%20Monad%3CIdentity%2C%20A%3E%20%7B%0D%0A%20%20constructor(private%20readonly%20value%3A%20A)%20%7B%7D%0D%0A%0D%0A%20%20chain%3CB%3E(f%3A%20(%3A%20A)%20%3D%3E%20Identity%3CB%3E)%3A%20Identity%3CB%3E%20%7B%0D%0A%20%20%20%20return%20f(this.value)%3B%0D%0A%20%20%7D%0D%0A%20%20fold%3CB%3E(f%3A%20(%3A%20A)%20%3D%3E%20B)%3A%20B%20%7B%0D%0A%20%20%20%20return%20f(this.value)%3B%0D%0A%20%20%7D%0D%0A%20%20map%3CB%3E(f%3A%20(%3A%20A)%20%3D%3E%20B)%3A%20Identity%3CB%3E%20%7B%0D%0A%20%20%20%20return%20new%20Identity(f(this.value))%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0Aconst%20identity%20%3D%20%3CA%3E(a%3A%20A)%3A%20Identity%3CA%3E%20%3D%3E%20new%20Identity(a)%3B%0D%0A%0D%0Ainterface%20TypesDictionary%3CF%2C%20A%20%3D%20never%3E%20%7B%0D%0A%20%20Identity%3A%20%7B%0D%0A%20%20%20%20param%3A%20F%20extends%20Identity%3Cinfer%20A%3E%20%3F%20A%20%3A%20never%3B%0D%0A%20%20%20%20type%3A%20F%20extends%20Identity%20%3F%20Identity%3CA%3E%20%3A%20never%3B%0D%0A%20%20%7D%3B%0D%0A%7D%0D%0A%0D%0A%2F%20Maybe%20%2F%0D%0Aabstract%20class%20Maybe%3CA%20%3D%20any%3E%20implements%20Monad%3CMaybe%2C%20A%3E%20%7B%0D%0A%20%20abstract%20maybe%3CB%3E(b%3A%20B%2C%20f%3A%20(%3A%20A)%20%3D%3E%20B)%3A%20B%3B%0D%0A%20%20chain%3CB%3E(f%3A%20(%3A%20A)%20%3D%3E%20Maybe%3CB%3E)%3A%20Maybe%3CB%3E%20%7B%0D%0A%20%20%20%20return%20this.maybe(nothing%2C%20f)%3B%0D%0A%20%20%7D%0D%0A%20%20map%3CB%3E(f%3A%20(%3A%20A)%20%3D%3E%20B)%3A%20Maybe%3CB%3E%20%7B%0D%0A%20%20%20%20return%20this.maybe(nothing%2C%20x%20%3D%3E%20just(f(x)))%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0Aclass%20Just%3CA%3E%20extends%20Maybe%3CA%3E%20%7B%0D%0A%20%20constructor(private%20readonly%20value%3A%20A)%20%7B%0D%0A%20%20%20%20super()%3B%0D%0A%20%20%7D%0D%0A%20%20maybe(%2C%20f)%20%7B%0D%0A%20%20%20%20return%20f(this.value)%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0Aconst%20just%20%3D%20%3CA%3E(a%3A%20A)%3A%20Maybe%3CA%3E%20%3D%3E%20new%20Just(a)%3B%0D%0A%0D%0Aclass%20Nothing%20extends%20Maybe%3Cnever%3E%20%7B%0D%0A%20%20maybe(b)%20%7B%0D%0A%20%20%20%20return%20b%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0Aconst%20nothing%3A%20Maybe%3Cnever%3E%20%3D%20new%20Nothing()%3B%0D%0A%0D%0Ainterface%20TypesDictionary%3CF%2C%20A%20%3D%20never%3E%20%7B%0D%0A%20%20Maybe%3A%20%7B%0D%0A%20%20%20%20param%3A%20F%20extends%20Maybe%3Cinfer%20A%3E%20%3F%20A%20%3A%20never%3B%0D%0A%20%20%20%20type%3A%20F%20extends%20Maybe%20%3F%20Maybe%3CA%3E%20%3A%20never%3B%0D%0A%20%20%7D%3B%0D%0A%7D%0D%0A%0D%0A%2F%20Either%20%2F%0D%0Aabstract%20class%20Either%3CL%20%3D%20any%2C%20R%20%3D%20any%3E%20implements%20Monad%3CEither%3CL%3E%2C%20R%3E%20%7B%0D%0A%20%20abstract%20either%3CB%3E(f%3A%20(%3A%20L)%20%3D%3E%20B%2C%20g%3A%20(%3A%20R)%20%3D%3E%20B)%3A%20B%3B%0D%0A%0D%0A%20%20chain%3CB%3E(f%3A%20(%3A%20R)%20%3D%3E%20Either%3CL%2C%20B%3E)%3A%20Either%3CL%2C%20B%3E%20%7B%0D%0A%20%20%20%20return%20this.either(left%2C%20f)%3B%0D%0A%20%20%7D%0D%0A%20%20map%3CB%3E(f%3A%20(_%3A%20R)%20%3D%3E%20B)%3A%20Either%3CL%2C%20B%3E%20%7B%0D%0A%20%20%20%20return%20this.either%3CEither%3CL%2C%20B%3E%3E(left%2C%20x%20%3D%3E%20right(f(x)))%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0Aclass%20Left%3CA%3E%20extends%20Either%3CA%2C%20never%3E%20%7B%0D%0A%20%20constructor(private%20readonly%20_value%3A%20A)%20%7B%0D%0A%20%20%20%20super()%3B%0D%0A%20%20%7D%0D%0A%20%20either(f)%20%7B%0D%0A%20%20%20%20return%20f(this._value)%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0Aconst%20left%20%3D%20%3CA%3E(a%3A%20A)%3A%20Either%3CA%2C%20never%3E%20%3D%3E%20new%20Left(a)%3B%0D%0A%0D%0Aclass%20Right%3CA%3E%20extends%20Either%3Cnever%2C%20A%3E%20%7B%0D%0A%20%20constructor(private%20readonly%20value%3A%20A)%20%7B%0D%0A%20%20%20%20super()%3B%0D%0A%20%20%7D%0D%0A%20%20either(%2C%20g)%20%7B%0D%0A%20%20%20%20return%20g(this.value)%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0Aconst%20right%20%3D%20%3CA%3E(a%3A%20A)%3A%20Either%3Cnever%2C%20A%3E%20%3D%3E%20new%20Right(a)%3B%0D%0A%0D%0Ainterface%20TypesDictionary%3CF%2C%20A%20%3D%20never%3E%20%7B%0D%0A%20%20Either%3A%20%7B%0D%0A%20%20%20%20param%3A%20F%20extends%20Either%3Cany%2C%20infer%20R%3E%20%3F%20R%20%3A%20never%3B%0D%0A%20%20%20%20type%3A%20F%20extends%20Either%3Cinfer%20L%3E%20%3F%20Either%3CL%2C%20A%3E%20%3A%20never%3B%0D%0A%20%20%7D%3B%0D%0A%7D%0D%0A%0D%0A%2F%20Static%20methods%20%2F%0D%0Aconst%20chain%20%3D%20%3C%0D%0A%20%20F%20extends%20RegisteredTypes%20%26%20Monad%3CF%2C%20A%3E%2C%0D%0A%20%20A%20extends%20TypeParamOf%3CF%3E%2C%0D%0A%20%20B%0D%0A%3E(%0D%0A%20%20f%3A%20(%3A%20A)%20%3D%3E%20Monad%3CF%2C%20B%3E%2C%0D%0A%20%20fa%3A%20F%0D%0A)%3A%20Generic%3CF%2C%20B%3E%20%3D%3E%20fa.chain(f%20as%20any)%3B%0D%0Aconst%20map%20%3D%20%3C%0D%0A%20%20F%20extends%20RegisteredTypes%20%26%20Functor%3CF%2C%20A%3E%2C%0D%0A%20%20A%20extends%20TypeParamOf%3CF%3E%2C%0D%0A%20%20B%0D%0A%3E(%0D%0A%20%20f%3A%20(_%3A%20A)%20%3D%3E%20B%2C%0D%0A%20%20fa%3A%20F%0D%0A)%3A%20Generic%3CF%2C%20B%3E%20%3D%3E%20fa.map(f)%3B%0D%0A%0D%0A%2F%20Examples%20%2F%0D%0Aconst%20x%20%3D%20right(%22Hello%20World%22)%20as%20Either%3Cnumber%2C%20string%3E%3B%0D%0Aconst%20y%20%3D%20map(val%20%3D%3E%20val.length%2C%20x)%3B%0D%0Aconst%20z%20%3D%20chain(val%20%3D%3E%20(val%20%3C%205%20%3F%20right(val)%20%3A%20left(val))%2C%20y)%3B%0D%0A) - working Either Monad.
I changed the library implementation a bit, I think the whole Push/Pop is unecessary (at least for the use cases I can think of).
Hey @nadameu, that's some very clean code, I like it. The problem with the solution you represent is that it can only work with one dynamic parameter. For example, what if I want to have a function that mutates the Left of the Either? You only register the right side into the TypeDictionary, which is fine for something simple like monads, but wouldn't work for a lot of use cases.
Here's a very common one, what if I want to map over a Tuple with 5 elements of different types? I have to be able to mutate all 5 types. The example code here is already more complex than your example could handle, once we'd start adding some methods onto the GenericEither that don't just do monadic operations. Note that the GenericEither presented by @masaeedu is one abstraction level higher than your concrete Either.
That said, if all you're writing is a monad library, the example you represent is quite a nice way to go about it. fp-ts takes a similar solution that's even simpler.
The repository now contains both Either and Maybe samples. I'll update the issues with Playground code once TypeScript 2.9 comes out, as I'm using some of the new keyof
improvements, but you can clone the repo and try it out right now (make sure to set your IDE to use the workspace version of TypeScript). It's not very pretty at certain points for Either, and it needs some gentle coercing here and there, but it all works. Hopefully, I can slowly improve upon this in the future.
Both examples are now available in examples/pattern-matching
Maybe.js:
Either.js:
Playground: With TypeProps