Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reconsider TypeScript support #446

Open
davidchambers opened this issue Oct 2, 2017 · 27 comments
Open

reconsider TypeScript support #446

davidchambers opened this issue Oct 2, 2017 · 27 comments

Comments

@davidchambers
Copy link
Member

davidchambers commented Oct 2, 2017

Update (2017-10-03): I have updated chainCurriedFlipped to delay the introduction of B, as suggested by @gcanti. I have also updated the results to correct an unrelated error I made initially (providing arguments to chainCurriedFlipped in the wrong order).


@miangraham and I have been working on #431 for several weeks. We've made good progress despite the impedance mismatch between the two type systems.

I had trouble defining types for chain. To simplify the task I specialized the type signature:

chain :: (a -> Array b) -> Array a -> Array b

I couldn't get this to work. I asked for help in the TypeScript room on Gitter and @sluukkonen provided helpful responses (including a link to microsoft/TypeScript#17520). I learnt that certain combinations of removing currying, changing the argument order, and replacing id with x => x (!) would appease the compiler.

I defined four versions of “chain” to discover the magic combination:

const xss = [[1, 2], [3, 4], [5, 6]];

function id<A>(x: A): A { return x; }

function chain<A, B>(f: (x: A) => Array<B>, xs: Array<A>): Array<B> {
  return Z.chain(f, xs);
}
function chainFlipped<A, B>(xs: Array<A>, f: (x: A) => Array<B>): Array<B> {
  return Z.chain(f, xs);
}
function chainCurried<A, B>(f: (x: A) => Array<B>): (xs: Array<A>) => Array<B> {
  return xs => Z.chain(f, xs);
}
function chainCurriedFlipped<A>(xs: Array<A>): <B>(f: (x: A) => Array<B>) => Array<B> {
  return f => Z.chain(f, xs);
}

Results of applying functions to id and xss

Function Result Value/Explanation
chain Argument of type <A>(x: A) => A is not assignable to parameter of type (x: {}) => {}[].
chainFlipped [1, 2, 3, 4, 5, 6]
chainCurried Argument of type <A>(x: A) => A is not assignable to parameter of type (x: {}) => {}[].
chainCurriedFlipped [1, 2, 3, 4, 5, 6]

Results of applying functions to xs => xs and xss

Function Result Value/Explanation
chain [1, 2, 3, 4, 5, 6]
chainFlipped [1, 2, 3, 4, 5, 6]
chainCurried Argument of type (xs: {}) => {} is not assignable to parameter of type (x: {}) => {}[].
chainCurriedFlipped [1, 2, 3, 4, 5, 6]

My reactions to the tables above:

  • 😱 id and xs => xs are not equivalent
  • 😞 TypeScript does not like curried functions
  • 😠 TypeScript's limitations affect API design

I'm concerned that even if we succeed in approximating type classes with interfaces, complications such as these will make Sanctuary and TypeScript an awkward couple. What do others think?

Sanctuary was designed as a JavaScript library. Had we set out to create a functional programming library for TypeScript it's likely we would have done things quite differently. I'm concerned that treating both JavaScript and TypeScript as first-class targets will lead to compromises. I'd like to continue to focus on making Sanctuary a principled, capable JavaScript library. Perhaps the TypeScript type definitions should live elsewhere so TypeScript's limitations won't influence the design of our API.

@gcanti
Copy link

gcanti commented Oct 2, 2017

chainCurriedFlipped works better if typed like this

declare function chainCurriedFlipped<A>(xs: Array<A>): <B>(f: (x: A) => Array<B>) => Array<B>

Results: 3 cases out of 8 require explicit type annotations

const id = <A>(a: A): A => a

declare function chain<A, B>(f: (x: A) => Array<B>, xs: Array<A>): Array<B>
declare function chainFlipped<A, B>(xs: Array<A>, f: (x: A) => Array<B>): Array<B>
declare function chainCurried<A, B>(f: (x: A) => Array<B>): (xs: Array<A>) => Array<B>
declare function chainCurriedFlipped<A>(xs: Array<A>): <B>(f: (x: A) => Array<B>) => Array<B>

const xss = [[1, 2], [3, 4], [5, 6]]

const r1 = chain(id, xss) // ERROR, chain<number[], number>(id, xss) works
const r2 = chainFlipped(xss, id) // ok
const r3 = chainCurried(id)(xss) // ERROR, chainCurried<number[], number>(id)(xss) works
const r4 = chainCurriedFlipped(xss)(id) // ok

const r5 = chain(x => x, xss) // ok
const r6 = chainFlipped(xss, x => x) // ok
const r7 = chainCurried(x => x)(xss) // ERROR, chainCurried((x: number[]): number[] => x)(xss) works
const r8 = chainCurriedFlipped(xss)(x => x) // ok

@davidchambers
Copy link
Member Author

Thanks very much for the improvement, @gcanti.

Results: 3 cases out of 8 require explicit type annotations

Is this common in the TypeScript world? Have you spent enough time with TypeScript that you reason about when hints will be necessary, or do you assume that types will be inferred and find it frustrating when they are not? I've never worked on a TypeScript project so I'm keen to hear from you and others with relevant experience.

@gcanti
Copy link

gcanti commented Oct 3, 2017

I design the API specifically in order to maximize type inference, though some cases are still frustrating (the identity function above being an example)

@jasonkuhrt
Copy link

@davidchambers I know you used to be more enthusiastic about FlowType, where do you stand these days? The project picked up some steam in the last quarter or two.

@CameronFraser
Copy link

+1 for flow. The windows support is still trailing, but it is picking up a lot of steam. Both are great options though. With babel 7 you can use TS as a type checker like flow instead of a compiler.

@davidchambers
Copy link
Member Author

@jasonkuhrt and @defdata, I'm pleased to hear that Flow is gaining momentum. :)

Can Flow describe functions such as S.chain? If so we should create a library definition for Sanctuary.

@cortopy
Copy link

cortopy commented Nov 28, 2017

@davidchambers looking at your typescript definitions looks like you did a lot of work even if you weren't entirely sure it could be done. Thank you!

I work in typescript projects and I wouldn't like this to go to waste even if you decide TS/Sanctuary are not a good match.

What about publishing the types in the official TS repo for now? This way it would be easier for the community to test types and even make contributions by just doing yarn add @types/sanctuary

@cortopy
Copy link

cortopy commented Jan 10, 2018

Just following on my last comment, I have published the types in DefinitelyTyped.

The first commit was just a port of what @davidchambers had started (thanks again for that!), except I made all the changes that tslint asked me for and some rather minimalist tests.

Beyond that, I've started to expand the declarations. Today I moved all methods into namespaces to be able to work with environments. The create method is now fully typed! (merge request now pending)

@davidchambers
Copy link
Member Author

The create method is now fully typed!

That's fantastic, @cortopy. Well done!

I apologize for my lack of engagement in this thread. I've been busy with other Sanctuary tasks, and in recent weeks with other life tasks. I'm thrilled to see that the effort @miangraham and I spent working on TypeScript type definitions has not gone to waste. :)

@anurbol
Copy link

anurbol commented Aug 29, 2018

Hi there! I liked so much the philosophy and description of Sanctuary. As much as I liked it, I was disappointed by the current type-checking system. While you obviously have done great work making it, it seems it is a wrong way, unfortunately. Typescript is trending so much. Unfortunately I am not able to contribute to this great library yet, but I just wanted to give my opinion about type system: Typescript definitely should be first-class-supported, this way I think this repo could gain 10x more stars.

For now I personally can not even start to work with Sanctuary because of the lack (AFAICT) of TS support. But again, your vision, @davidchambers is oh so appealing... but type checking is disappointing..

Also, I personally think type-defs are wrong way in terms of both correctness and maintainability, it is much easier to khm.. rewrite the code to typescript.

BTW, Sanctuary has to deal with very complex types. If someone is interested I have a library for testing such complex types: https://github.com/qurhub/qur-typetest, helps me a lot.

@davidchambers
Copy link
Member Author

For now I personally can not even start to work with Sanctuary because of the lack (AFAICT) of TS support.

Have you tried DefinitelyTyped, @anurbol?

Also, I personally think type-defs are wrong way in terms of both correctness and maintainability, it is much easier to khm.. rewrite the code to typescript.

Once TypeScript can express Functor f => (a -> b) -> f a -> f b, I'll certainly consider it! I'm not holding my breath, though. 😜

@gabejohnson
Copy link
Member

gabejohnson commented Aug 29, 2018

@davidchambers have you discussed collaboration with @gcanti and @alexandru on a shared HKT lib?

I believe https://github.com/gcanti/fp-ts and https://github.com/funfix/funfix share a common approach.

For reference:
funfix/funfix.js#104
gcanti/fp-ts#252

@anurbol
Copy link

anurbol commented Aug 29, 2018

@davidchambers I am sure Typescript is capable of handling the most complex scenarios possible.

I apologize in advance, because I am not familiar well with haskell-like syntax except I've read "types" section of the Sanctuary docs, I am also just in the beginning of my FP journey (this is the reason why I want to choose the best FP library to start with), but if I understood correctly

Functor f => (a -> b) -> f a -> f b is the same as the following in TS

type Functor<T> = (arg: T) => any

/* The following is equivalent to (a -> b) -> f a -> f b */
<A, B>( arg: (a: A) => B ) => (arg: Functor<A>) => Functor<B>

BTW. Thanks to your challenge, I got familiar with Haskell syntax, and I must say it is much more concise and elegant... i.e. I found out that I don't really know what argument names like arg: in the above code are actually doing (they are unnecessary noise). However, I think, probably Typescript's type system is more powerful than Haskell's one. E.g. I doubt (googled briefly) haskell has conditionals like type Foo<T> = T extends string ? string : T extends number ? number : never and the reason is: TS was created to be capable of producing types for any case in such a dynamic language like JS.

@Bradcomp
Copy link
Member

@anurbol I think you have a little too much faith in Typescript. I use TS professionally at my job, and it lacks a lot of features that would make functional programming feel natural. One of these is Higher Kinded Types, which TS does not support directly, although there are some work arounds (see below): microsoft/TypeScript#1213

Haskell's type system is strictly more power than TS. You can represent any TS construct in Haskell's type system, but the opposite is not true.

What you have above assumes that Functor is a type, whereas it is in fact a type class, more similar to an interface than a class or a type.

This means that if you have three generic types: A<T>, B<T>, and C<T> and you want them all to be functors, the map function (forgive my pseudo code here) needs to be generic over both T and [A|B|C] such that

map(T -> U)(A<T>) => A<U>
map(T -> U)(B<T>) => B<U>
map(T -> U)(C<T>) => C<U>

We need to be able to represent that (and even more complicated notions) in the type system, without defaulting to using any.

I would suggest playing around with implementing it yourself, and then taking a look here: https://github.com/gcanti/fp-ts to see one way it's been tackled.

I would also suggest taking a look at the types @davidchambers posted above. You can see that the developer simply punted on the issue of Higher Kinded types, leaving the actual type checking to Sanctuary.

In the past few months I have run into numerous issues in typescript trying to do things that would be trivial with Sanctuary. In addition, there are many errors that it won't help you catch at all. For instance, exceptions aren't represented in the type system, so it's not possible to know if a particular function can blow up your program.

In addition, the fact that Sanctuary checks types at runtime gives it even more power. I have tried to use decorators to modify the type of a method at runtime. It's impossible in Typescript because TS only has compile time information. Sanctuary doesn't have this limitation.

If you're truly interested in using functional programming in JS, I don't think Typescript is in any way ideal. I would suggest using regular JS along with libraries like Sanctuary or Ramda, or else go whole hog and check out a language like Elm or Purescript which have been built from the ground up to facilitate strongly typed functional programming that compiles to Javascript.

@masaeedu
Copy link
Member

masaeedu commented Aug 29, 2018

@anurbol A simple definition of Functor in pseudo-TypeScript would be:

type Functor<F> = { map: <A, B>(f: (a: A) => B) => (fa: F<A>) => F<B> }

In other words, a map function, that for some F, can convert any A -> B into an F<A> -> F<B>. Things like this are inexpressible in TypeScript, due to the lack of higher kinded types, partial type application, generic values, and numerous other features. There are various workarounds that work to various extents, but they add complexity and detract from the simple, beautiful shape of the abstractions we want to work with.

If you're interested in good TypeScript support for functional programming libraries (something I think many users are very interested in!) you should find the relevant issues in the TypeScript issue tracker and provide your feedback.

@anurbol
Copy link

anurbol commented Aug 29, 2018

@Bradcomp, @masaeedu thank you guys. This is why I apologised in advance because I did feel that I may not be right. FP seems much deeper than I thought. That's a pity TS does not provide so many required features. Funny I thought there is almost nothing left that TS can't cover! 😄

@gabejohnson
Copy link
Member

@pelotom recently put https://github.com/pelotom/hkts up as well. I haven't used it, but it looks very concise.

@anurbol
Copy link

anurbol commented Aug 31, 2018

I want to apologize once again, I've looked how much work was done on sanctuary's type system, and it looks like a piece of art.

@davidchambers
Copy link
Member Author

Thanks for your words of encouragement, @anurbol. 😊

@RichardForrester
Copy link

I just want to make sure that Sanctuary’s authors are aware of the recent typescript 3.4 release notes that include “higher order type inference from generic functions.”

Sanctuary looks wonderful. The only thing holding me back is the lack of typescript support.

@davidchambers
Copy link
Member Author

Thank you, @RichardForrester. It seems we should have another go at writing type definitions. :)

@RichardForrester
Copy link

RichardForrester commented May 27, 2019

This library really hits a sweet spot for me -- as FP as you can get without ditching the NPM eco-system. I'm vegging out today writing a bunch of .ts unit tests trying to get the hang of the api. It's very nice so far.

I also installed @types/sanctuary and it seems to be working fine.

This is obviously just my opinion, the perspective of a ts user who is strongly considering adopting Sanctuary.js as my main daily driver, but I think as far as Typescript support goes, I honestly don't care about having super fantastic typings because it's probably still impossible to even have good ones and that's fairly common with even some great libraries that put a lot of time into trying to get typings perfect. My ramda code is riddled with as any and // @ts-ignore and that's fine with me because I'd rather spend my time writing my code rather that trying to figure out why exactly the typings are wrong. Sure they catch some errors that I miss here and there, but if I spend all my time tracking down all the weird things I see TS complaining about I'll never get anything done.

However, basic typescript support, to allow for auto import and some IntelliSense with VSCode goes a long way. The truth for me, and probably a lot (if not most) people using Typescript, is that I use it for the little conveniences, especially with VSCode. I'm talking about renaming functions across your project with a single command, moving files and having all of the related imports automatically updated, auto import when typing some function name. Once you get used to those things, you can't go back. But the actual typings are kind of pain... they are wrong more than my code is, meaning that a lot of debugging time turns out spent on fixing problems with typings or finding escape hatches for bad typings and not with the javascript. Still, when it comes to refactoring, I will never go back to plain javascript.

I think you are dead right when you say that you worry that the implementation of Sanctuary.js should not be affected by trying to satisfy the limitations of Typescript. You should stay true to that and if it makes sense to you to include some basic type definitions for enabling those free wins you get with TS it should be okay.

But if you have to choose between spending ?? hours writing and maintaining near perfect typings that will never be exactly what you want them to be, and improving the actual library, I'd say the @types/sanctuary is already there and works fine; keep working on the actual library - in other words, features like supporting transducers or tree traversal should be a higher priority than making typings better. I can tell a lot of love and perfectionist kind of thinking has gone into it. Applying that sort of perfectionist attitude toward .ts typings would be enough to drive anyone crazy.

@ceigey
Copy link

ceigey commented Jun 26, 2019

@RichardForrester thank you mentioning that the types are working well - I've been on the fence about them because the first line is

// Type definitions for sanctuary 0.14

(and it looks like Sanctuary's at v2 now - wow! Congrats!)

But perhaps it's worth waiting for the hypothetical day TypeScript implements something that lets pipes type properly without using casts and assertions. That might be a big ask of the TS team though, since it's a non-trivial problem.

(There's also Gcanti's fp-ts and io-ts which can do some of the things Sanctuary does like Maybe/Option types, and type checking arbitrary objects (~sanctuary-def) that might be a more burning need for TS programmers in the interim)

@elie222
Copy link

elie222 commented Aug 14, 2019

This may help:
https://gist.github.com/elie222/e34a6ef6b818822bda17d0bebfff5c7c

@dotnetCarpenter
Copy link

dotnetCarpenter commented Mar 24, 2021

I know this thread is old but I want to chime in with how I just got IntelliSense support in vscode for Sanctuary.

I'm developing a js project using vscode and sanctuary via jsDelivr1.

First I installed @types/sanctuary via npm i -D @types/sanctuary. Then, to get vscode to check my files, I created a jsconfig.json with the following:

{
	"compilerOptions": {
		"checkJs": true
	},
	"exclude": ["node_modules", "**/node_modules/*"]
}

In my index.htm I got:

<script src="https://cdn.jsdelivr.net/gh/sanctuary-js/sanctuary@3.1.0/dist/bundle.js"></script>
<script type="module" src="js/app.js"></script>

and in js/app.js I got:

/** @type {import('sanctuary')} */
const S = window.sanctuary
const $ = window.sanctuaryDef

Now I get code completion on S. but vscode still complains that Property 'sanctuary' does not exist on type 'Window & typeof globalThis'.ts(2339) and provides a red wave underline under sanctuary.

I fixed this by creating a globals.d.ts in a subfolder, but according to vscode documentation it can be anywhere in your project.

In globals.d.ts I have:

interface Window {
	sanctuary: import('sanctuary');
	sanctuaryDef: import('sanctuary-def');
}

The import('sanctuary'); does not seem to work - in the sense that, that alone will not enable IntelliSense for Sanctuary. But it does remove the two error messages from window.sanctuary, window.sanctuaryDef and show that the two properties exist on the Window object.

image

That is as far as I got. For window.sanctuaryDef, I do not know what to do. There does not seem to be any mention of these predicates types in https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/sanctuary/index.d.ts.

But this is way better than nothing! And hopefully helpful for someone else :)

1: Unfortunately there is no default file set, which seems to be a prerequisite for sha256 integrity on jsDelivr.

@dotnetCarpenter
Copy link

I got some help by @jcalz at https://stackoverflow.com/q/66817628/205696, to get intellisense support in vscode for sanctuary-def and sanctuary-type-identifiers.

Now my global.d.ts looks like this:

import * as S from 'sanctuary'
import sanctuaryDef from 'sanctuary-def'
import sanctuaryTypeIdentifiers from 'sanctuary-type-identifiers'

declare global {
	interface Window {
		sanctuary: typeof S;
		sanctuaryDef: typeof sanctuaryDef;
		sanctuaryTypeIdentifiers: typeof sanctuaryTypeIdentifiers;
	}
}

That means I do not have to define the type for Sanctuary with /** @type {import('sanctuary')} */.
But this only works because I have installed @types/sanctuary. Unfortunately, these typings does not exist for sanctuary-def and sanctuary-type-identifiers. So I had to install both libraries to node_modules, even though I do not use them (I use https://cdn.jsdelivr.net/gh/sanctuary-js/sanctuary@3.1.0/dist/bundle.js exclusively).

"devDependencies": {
    "@types/sanctuary": "^3.0.3",
    "sanctuary-def": "^0.22.0",
    "sanctuary-type-identifiers": "^3.0.0"
  }

@davidchambers
Copy link
Member Author

Thank you for sharing your workaround, @dotnetCarpenter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests