-
-
Notifications
You must be signed in to change notification settings - Fork 95
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
TypeScript support #431
base: main
Are you sure you want to change the base?
TypeScript support #431
Conversation
Thank you very much for your feedback, @tycho01. π
None of the methods specified by Fantasy Land is curried, so I believe this to be correct.
I wanted the specialized signatures to resemble the general signatures as closely as possible, and assumed the unused type variable would simply be ignored. Is my assumption correct?
I've resigned myself to the file being at least 1000 lines. I'm not overly bothered by this, provided we having tooling in place to catch errors. Based on the comment from @ikatyang we should be able to achieve this.
This is faithful to Sanctuary's behaviour. Any type with a
In accordance with #424 the
The predicate takes one argument. |
index.d.ts
Outdated
} | ||
interface Alternative<A> extends Applicative<A>, Plus<A> { | ||
constructor: ApplicativeTypeRep<A> & PlusTypeRep<A> | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unhappy with this definition. I had hoped that
interface Alternative<A> extends Applicative<A>, Plus<A> {}
would suffice, but the constructor
property requirements are seen to conflict rather than combine.
Is there an elegant solution to this problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for what it's worth, I'm not currently aware of any.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, you could probably abstract it into a pattern I guess. But yeah only really pays off given multiple occurrences of this.
Yeah, you're right, they are.
Yeah, agreed, their approach looks good there. For Ramda typings it added conciliating with run-time tests over
That's what one might assume looking at the Ramda docs as well. But in practice they just iterate over arrays using
I'll concede that may make for a trade-off between TS UX and compliance with docs. I guess that was a reason I kinda gave up on trying to make them match for the Ramda typings -- I never really tried since the guys before me hadn't either, but I fear it may be tough to always keep the number of generics equal. These two examples aside, in TS I've had the impression it's easy to end up with more. Maybe for UX here having it say e.g.
Well, I'd gotten type-classes wrong on my try, so I think you're doing pretty good! π |
index.d.ts
Outdated
export function equals<A>(x: Error): (y: Error) => boolean; | ||
// XXX: This is too general. Can we match "plain" objects only? | ||
// export function equals<A>(x: Object, y: Object): boolean; | ||
// export function equals<A>(x: Object): (y: Object) => boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have a suggestion, @tycho01?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's object
and {}
. I haven't even bothered to look into their nuances. Object
appears not used as it'd match almost anything.
On DRY'ing... equals<A>(x: any, y: any): boolean
? π
Similar to the lt
/ gt
; wanted to say there's equals<A extends null | undefined | boolean | ...>(x: A, y: A): boolean
(or even drop the constraint), but... generics don't just fail with literals here, it just does nothing useful. At that point it'll just be like, equals(123, "foo")
totally works for A
: any
(or for number | string
).
So technically without run-time type checks equals(123, "foo")
may still run and produce false I guess?
I'd wanna go for a type-level type check, but those aren't possible yet. Failing that, it's your solution or any. If you'd consider not doing run-time errors for different types, with your approach you might even add a equals<A>(x: any, y: any): false
fallback at the bottom.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On DRY'ing...
equals<A>(x: any, y: any): boolean
?
This is feasible for S.equals
but not for S.map
and other polymorphic functions which can operate on values of type StrMap a
(plain objects with consistently typed values).
At that point it'll just be like,
equals(123, "foo")
totally works forA
:any
(or fornumber | string
).
That's unfortunate. Although sanctuary-def provides $.Any
this is not considered to be a "unifying" type. It's a pity that TypeScript can't express types such as a -> a -> a
.
If you'd consider not doing run-time errors for different types, with your approach you might even add a
equals<A>(x: any, y: any): false
fallback at the bottom.
Is it possible to include implementations in a .d.ts
file? Keep in mind that the implementation is still written in a .js
file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you could probably do is store the type using the generic as a generic type, then type the function using an interface of overloads populated by instances of this generic type, which would... maybe look a bit cleaner. I never tried anything like it though.
type Equals<A> = (x: any, y: any): boolean;
then like interface equals { Equals<string>; ... }
I'm on phone atm so can't test well, pretty sure it'd fail but it's the best improvement I can come up with. So probably just gonna be the way you got it now then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For map consider checking the definition used in the Ramda typings.
What do you mean on the .d.ts vs. js?
Putting JS in a .d.ts file, I forgot if I tried, so don't take my word for it, but it probably fails.
Having js in the .d.ts seems not to make much sense though, so I'm probably misinterpreting what you mean by implementation.
Sanctuary has no notion of "array-like" objects: S.map(S.toUpper, {'0': 'foo', '1': 'bar', '2': 'baz', length: 3});
// ! TypeError: Type-variable constraint violation
//
// map :: Functor f => (a -> b) -> f a -> f b
// ^^^
// 1
//
// 1) {"0": "foo", "1": "bar", "2": "baz", "length": 3} :: Object, StrMap ???
//
// Since there is no type of which all the above values are members, the type-variable constraint has been violated. If one evaluates this expression with Sanctuary's run-time type checking disabled, |
index.d.ts
Outdated
(a: A, b: B, c: C): R | ||
(a: A, b: B): (c: C) => R | ||
(a: A): AwaitingTwo<B, C, R> | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've seen these before somewhere. π€
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...and they may be problematic: #431 (comment) :(
index.d.ts
Outdated
|
||
export function bimap<A, B, C, D>(f: (A) => B, g: (C) => D, bifunctor: Bifunctor<A, C>): Bifunctor<B, D>; | ||
export function bimap<A, B, C, D>(f: (A) => B, g: (C) => D): (bifunctor: Bifunctor<A, C>) => Bifunctor<B, D>; | ||
export function bimap<A, B, C, D>(f: (A) => B): AwaitingTwo<(C) => D, Bifunctor<A, C>, Bifunctor<B, D>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notice that TS won't infer types across the function call, this kind of function should be something like:
-export function bimap<A, B, C, D>(f: (a: A) => B): AwaitingTwo<(c: C) => D, Bifunctor<A, C>, Bifunctor<B, D>>;
+export function bimap<A, B>(f: (A) => B): {
+ <C, D>(x: (c: C) => D, y: Bifunctor<A, C>): Bifunctor<B, D>;
+ <C, D>(x: (c: C) => D): (y: Bifunctor<A, C>) => Bifunctor<B, D>;
+};
Otherwise the C
and D
will be inferred as {}
, since they are not exised in the first function call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, @ikatyang! I will make this change and similar changes then push another commit. I'd love you to review it. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some tools to restructure this kind of definition in my types-ramda, but it does not expose yet, I'll expose them ASAP to make restructure FP definitions easily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And it's recommended to setup a test first, since our eyes can't catch all the bugs, but tests can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been playing with the fully applied form of S.bimap
to understand your feedback, but TypeScript seems oblivious to type errors:
S.bimap(S.toUpper, Math.sqrt, S.Left(42))
Am I doing something wrong, or is TypeScript simply unifying everything to any
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps you could pull this branch and run these commands:
$ mkdir experiment
$ cd experiment
$ echo $'import * as S from "..";\n\nS.bimap(S.toUpper, Math.sqrt, S.Left(42));' >index.ts
$ tsc index.ts
$ cat index.js
"use strict";
exports.__esModule = true;
var S = require("..");
S.bimap(S.toUpper, Math.sqrt, S.Left(42));
Do you observe the same behaviour?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I know what you mean. The current branch haven't updated the (A) => B
to (a: A) => B
, so that A
is considered a variable name and its type is any
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can copy code from the playground link I mentioned above, and just put it in test.ts
then tsc test.ts
, you'll see the error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current branch haven't updated the
(A) => B
to(a: A) => B
, so thatA
is considered a variable name and its type isany
.
Aha! I overlooked that in your first comment. Thank you for being patient with me. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidchambers I've done my dts-element-fp for restructuring curried function definitions (placeholder available), might be useful to you.
index.d.ts
Outdated
// Placeholder | ||
|
||
interface Placeholder { | ||
'@@functional/placeholder': boolean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could simply use true
instead of boolean
. Wouldn't want Sanctuary to treat my value as a placeholder even though I explicitly stated that @@functional/placeholder
is false
!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true
is not a type, though, is it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true
is a literal type.
literal type: boolean
(true
, false
), number
(1
, 2
, etc.) and string
('str1'
, 'str2'
, etc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I change boolean
to true
, this code example does not type check:
import {concat} from 'sanctuary';
const _ = {'@@functional/placeholder': true};
concat(_, 'def')('abc');
Do you know what's wrong, @ikatyang? I'd love to use true
if possible. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For objects, TS always widen their type to non-literal, so in this case you have to assert it to true
manually, e.g.
const _ = {'@@functional/placeholder': true as true}; //=> {'@@functional/placeholder': true}
const _ = {'@@functional/placeholder': true}; //=> {'@@functional/placeholder': boolean}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That works! Thank you for the explanation. I'm enjoying learning from you. :)
Interested party here. Any ETA on a merge? Any specific help needed? I'm looking at giving Sanctuary a spin from a TS project and adding it to an "How to Do Immutable Typescript" starter package I'm working on. Can work directly from the davidchambers/typescript branch for a bit if I need to. Thoughts appreciated. |
Thank you for taking an interest, @miangraham, and for submitting a helpful pull request. I don't know when this pull request will be merged, as I'm focused on sanctuary-js/sanctuary-scripts#1 at present. This pull request is not blocked, as far as I recall, but there's still a lot to do:
@miangraham, if you're interested in contributing to this effort I will give you write access so you can push commits directly to |
@davidchambers This seems like good bang for buck and a spitball should be super easy. Something like davidchambers/typescript...miangraham:davidchambers/typescript could hit index.d.ts with strict type checking, tslint, and some new type-level tests ("this line should fail to type check in this way") from your existing hooks. Can def chip in some type defs too. Feel free to add me and I can try pushing a few things your way over the weekend. |
Excellent! Feel free to push to this branch. Please don't |
The last CircleCI pass included these:
For a tslint rules baseline I started with tslint-config-standard and turned off everything needed for index.d.ts to pass as-is. Big ones to consider turning back on: |
For reference, current obvious index.d.ts TODOs:
I got filter (with tests) working for Array without issue then bounced off the interface for non-arrays (I'm new to Fantasy Land). Will come back to it. In the meantime, Function and Logic look straightforward so I'll try grinding through those for a win. |
Monads aside there might well be overlap with ramda, so feel to take from there what you need. |
Many-param
I'll push |
@miangraham: if I can finish my current PRs overloads may no longer be needed (curry PoC at microsoft/TypeScript#5453 (comment)). |
@tycho01 Ooo, very nice. I'd seen Ramda's generation approach but didn't know about any of the TS work on variadic kinds. That looks like an awesome shortcut. Existing interfaces with multiple fixed arities would still need overloads but they could just be typed in terms of the variadic version, saving a ton of boilerplate. I think. |
@miangraham: Yeah. Ideally type programming should be no worse than the expression level. Heck, in the ideal scenario the standard library would be well-typed enough for it to just infer these return types rather than us having to do them manually. Much less with clunky overload codegen. |
Thanks for the contributions, @miangraham. Keep them coming. :) You may have an opinion on #438. If we decide to abandon Ramda-style currying our TypeScript type definitions will no longer be so unwieldy. |
I'm currently erring towards wide and shallow treatment to try and get an MVP/first pass done for the whole library. Means skimping on currying where it's not easy while leaving as many explicit TODOs as possible, but could result in getting something out fairly soon. I'm thinking this month if we're lucky? Going breadth-first also enables fundamental changes in approach. If the TS defs really want to bite off whole-hog fancy currying, that probably means code generation, a separate repo to cordon off the infrastructure to generate the final .d.ts, more exhaustive testing, etc etc etc. Plenty of great Ramda examples to lean on there when it's needed. In the meantime it'd be nice to get users just statically typing the basics to see how it goes! |
I agree. Let's focus on the curried signatures initially. :) |
e732768
to
a543011
Compare
π§ π·ββοΈ π§
Partially addresses #254
This is a work in progress, but as people are already providing useful feedback it makes sense to open a pull request for ongoing discussion.