bcherny/tsoption

Unable to use Option as type

Aankhen opened this issue · 8 comments

As a return type:

> import { Option, Some, None } from "tsoption"
{}
> function foo(bar: any): Option<boolean> { return Option.from(Option.from(bar).isEmpty()) }
undefined
> foo("abcd").map(s => s.toString())
Thrown: ⨯ Unable to compile TypeScript
[eval].ts (1,1): Cannot invoke an expression whose type lacks a call signature. Type '(<U = boolean>(f: (value: boolean) => U) => Some<U>) | (<U = boolean>(f: (value: boolean) => U) =...' has no compatible call signatures. (2349)
[eval].ts (1,17): Parameter 's' implicitly has an 'any' type. (7006)
>

As a parameter type:

> import { Option, Some, None } from "tsoption"
{}
> function foo(x: Option<boolean>): string { return x.getOrElse("none").toString() }
Thrown: ⨯ Unable to compile TypeScript
[eval].ts (1,51): Cannot invoke an expression whose type lacks a call signature. Type '(<U extends boolean>(def: U) => boolean) | (<U extends boolean>(def: U) => U)' has no compatible call signatures. (2349)
>

Is it just not possible to do this in TypeScript? I apologize if I’m missing something obvious. (I understand that the issue isn’t typing something as Option in and of itself but rather calling the methods on that value.)

Hi @Aankhen! What version of TypeScript are you using?

That said, there are cases where this is a known issue: microsoft/TypeScript#22597

Hi! Thanks for the pointer. This is with version 2.8.3. It does look like the same TypeScript bug. :-\ It’s unfortunate, because it severely limits where I can use Option.

Fixed in 0.5.0. I ended up rewriting as classes instead of functions to get around the TS bug. The API is updated a bit, so be aware of the small breaking changes!

That’s great, thank you. The new version works much better, but I’m having some trouble understanding why the Option#flatMap and Option#map return Option<T> | Option<U>. I tried changing them to Option<U> and the tests still pass, while allowing more simplified code. Again, sorry if I’m missing something obvious!

Mapping a None<T> over any function should return a None<T>. This makes it not quite a true monadic Option type, but I think it's more understandable and truer to what an Option means semantically. The monadic versions are available under the fantasyland methods as chain and map. What do you think? I'm no expert with this stuff.

I’m not an expert in the theory either, heh. Consider this, though:

function foo(x: Option<string>): Option<number> {
  return x.map(s => s.length)
}

With the current definition, this won’t compile, because the map returns Option<string> | Option<number>. I find this unexpected. I would intuitively think that any None returned is now a None<number> instead, meaning the map returns Option<number>. (I understand what you’re saying about the fantasyland methods. That seems non-ergonomic, though.)

I dug around and found that the other languages using this pattern mostly define Option as an enum, which takes the type parameter and uses it on the Some, but Rust, for example, still defines map as returning an Option<U>, and Haskell’s Data.Functor defines fmap as returning the new type too. Scala defines a ‘case object’ of type Option[Nothing], where Nothing is a subtype of every type. I don’t know whether one could do something similar with never in TypeScript.

@Aankhen Thanks for the example, and for the really helpful audit. 0.6.0 is published - how does that work for you?

Seems to work great now. Thank you for all the prompt responses and the effort to both write and then fix this. I’m really looking forward to simplifying my code!