raj
raj copied to clipboard
Typescript typings
I like the look of raj and want to try it out :-). However since I use typescript I cannot try anything until I have types. So here is my initial attempt at typings for raj:
export type Change<S, M> = [S, Effect<M>?];
export interface Effect<M> {
(dispatch: Dispatch<M>): void;
}
export interface Update<M, S> {
(message: M, state: S): Change<S, M>;
}
export interface View<M, S, V> {
(state: S, dispatch: Dispatch<M>): V;
}
export interface Dispatch<M> {
(message?: M): void;
}
export interface Done<S> {
(state: S): void;
}
export interface Program<S, M, V> {
readonly init?: Change<S, M>;
readonly update: Update<M, S>;
readonly view: View<M, S, V>;
readonly done?: Done<S>;
}
export function runtime<S, M, V>(program: Program<S, M, V>): void;
You can see them in use in my experiment repo.
Any feedback on the typings are welcome. It would be nice if they could be added to the package once they are reviewed fully.
@rluiten has made some progress on TypeScript support: https://github.com/rluiten/rajts
My main concern is that when working with HOPs (high-order-programs), particularly in raj-spa
the type safety breaks down as each program interface is so flexible. See a short discussion about that: https://github.com/andrejewski/raj-spa/issues/34
I have not given TypeScript a fair shake in a production app and since there exist alternatives such as Flow, I'll be much happier to see @types/raj
then have it in Raj core. Since this has come up in a few discussions, I think it's worth coming together on @types/raj-*
to just get experimenting with the best ways to model things and focus on being good enough so that we have a TypeScript story that people can get started with and refine over time.
I'm interested to see where this goes, thanks for taking the time to build these things out and your contributions to other repos, @jonaskello.
Yes, I did have a quick look at @rluiten repos but as I understand it that is a re-implementation in typescript rather than just types for the existing packages.
I've made types for raj, raj-react and some for raj-compose. So far it has been rather straightforward but I haven't gotten to raj-spa yet. I like the fact that there are separate packages though. I guess even if one package is hard to do typings for, that would not stop doing typings for the other ones? Also, I think it would be possible to do typings for HOP as it is possible to do for higher order components in react. I've done HOC typings and it is doable although a bit complicated.
About @types/*
I'm not sure how to get something published there but last I checked it was rather complicated. For the typings author and the consumer, having a index.d.ts
file in the package itself is easier to maintain and consume. But of course it will mean that fixes to the types will need a package re-publish which will put some more burden on the package maintainer.
Another point of interest might be that typescript types are not just for typescript users anymore, they provide value in other ways and I would tend to agree with this blog post even if it is highly opinionated :-).
Anyway, I'd be happy to do a PR to add an index.d.ts
file to the repo so raj could be used in typescript projects. If the types should live on @types/*
, I could provide the initial typings but it would be nice if someone else that knows how to handle that process could then maintain it.
I'm thinking about this. Some thoughts:
-
As a maintainer, I want to exercise my code and the experience around it. If I don't use TypeScript my only feedback loop is complaints and bug patches from users. As far as I'm concerned, Raj the framework is complete. It works with ES3+. TypeScript is a moving target that Raj would have to evolve with, which means someone is making a time investment.
-
Experimentation is an issue. In any
raj-*
package we can do typing sure, but that's because they are all0.0.x
software. Withraj
I hesitate to add types. -
Coupling is an issue. The
raj-*
libraries currently don't couple to Raj, but adding types they all start to have to know of Raj's existence. This is not ideal as custom runtimes are possible. This is a big downside of typing in general, it exposes more information than necessary. -
While Raj is the Elm architecture and Elm has types that works from them, Raj and JavaScript are a bit different. Not having to deal with types and instead working in terms of interfaces/contracts, we have a way better composition and reuse story. I suggest typing
raj-compose/batchPrograms
to see what I mean, which has the same problem asraj-spa
. -
It's important to note that
raj/runtime
andraj-react/program
are only used once in a full application. Perhaps having Dispatch, Effect, and Change in araj-types
package is enough to give TypeScript users enough to work with day-to-day.
Thanks for taking the time to contemplate this!
Just to clarify there are three ways we could add types:
- Put a
index.d.ts
file in the root of the repo and add atypes: index.d.ts
field topackage.json
. - Put a
index.d.ts
file in a separate package@types/raj
. - Rewrite the library in typescript and then compile to javascript (ES3) + typedefs.
In both (1) and (2) the types are totally separated from the javascript code, and no alterations to the javascript code is neeeded. The only difference is that (1) will make it more convienent to maintain and consume the types. This convience comes at the cost that changes to the types (ie. the index.d.ts
file) will require a republish of the raj
package rather than just the @types/raj
package. However if you have a stable and simple API such as for raj
, I don't see any reason why the types would need to change.
Regarding your first item I think those concerns only apply if we do alternative (3) above, but maybe I misunderstood? I would propose to do (1) above. I think I'll go ahead with a PR for (1) so that just to so you can concretely see the diff that it would incur. This PR would be for discussion only so don't feel inclined to have to merge it.
Regarding your second item, I've encountered this several times. I don't know if there is a term for it but I usually refer to it as "type coupling". As an example, I use redux-logger
a lot and I was thinking of forking it and making a raj-logger
which I would write in typescript and compile/publish to JS+typedefs. I have not given the implementation a lot of thought yet but my inital intuition is it would be a Higer Order Program that intercepts messages. This would require it to know the types of Program
but as there is no canonical type definition for Program
(as there are no types in the raj
package) I'm not sure how to proceed. I could put all the types for Program
and related types in the raj-logger
package, and then duplicate them in all other raj-*
packages I would craete, but duplication does not really feel like a good thing. On the other hand if there were types in the raj
package (or @types/raj
), then raj-logger
would need a dependency on the raj
package but only to get the types. The problem is that if raj-logger
s public API has types that concerns Program
, then it really has to have a hard dependency on the package that holds those types. However, even without "type coupling" you would have implicit contract coupling which arguable is worse.
Regarding your third item, I can see what you mean, but I don't follow how having types would hinder JS development as all JS code would be unaware of the types. OTOH not having types hinders all typescript projects for consuming raj
. Perhaps you can clarify this item a bit?
Regarding your fourth item, I would say having types that way is unfamiliar to the typescript community and hard to work with. Which is why the community sattled on either includeding them as a separate file in the package, or as a @types
package.
My own thinking is that the core raj
package will change very little and there will be very few breaking API changes. For this reason I think this is the easiest package to do types for. Using option (1) above we would add index.d.ts
file one time and then we are done for the forseeable future, and no javascript user would even notice the existance of the types.
Another thought is that types are basically executable documentation. The recommandation when writing type definitions for an existing JS package is to use the documentation as a base rather than the code. Since javascript has no notation of privacy, the only way to know which is the public API is the docs and the types should only surface the public API. When you would make an API change to a package, you would need to update the README file. Having types is the same thing, when you do an API change you would have to update the index.d.ts
file. Basically there should be no other reason for change of the typings file.
So I did a PR to support this discussion in #32.
Coupling is an issue. The raj-* libraries currently don't couple to Raj, but adding types they all start to have to know of Raj's existence. This is not ideal as custom runtimes are possible. This is a big downside of typing in general, it exposes more information than necessary.
opaque-types may address this concern. (e.g. https://codemix.com/opaque-types-in-javascript)
@pandorasNox Opaque types are definitely a good idea. I suggested it in the typescript repo #15408 and #15465 however it seems typescript will not implement them although flow has since then done it.
Coupling is an issue. The raj-* libraries currently don't couple to Raj, but adding types they all start to have to know of Raj's existence. This is not ideal as custom runtimes are possible. This is a big downside of typing in general, it exposes more information than necessary.
This is not necessarily true in TypeScript, I think?
Libraries don't necessarily need to depend on the TypeScript types in the raj
package - they only need to be compatible with them, as interfaces don't need to be explicitly implemented in TS.
For example:
interface Foo {
bar: string;
}
function blip(thing: { bar: string }) {
}
Here, the argument to blip()
doesn't depend on Foo
- but they're still compatible.
So if you don't want explicit coupling to the library in HOPs, you don't have to.
I think, when it comes to actual programs, the typings would still be useful though?
@mindplay-dk Good point That works because typescripts uses structural typing instead of nominal typing. We can take advantage of structural typing to have less type-coupling.
I attempted an opinionated port of Raj to TypeScript:
https://codesandbox.io/s/raj-ts-e27ju
This type-checks internally as well as type-checking your programs - this may not seem very important, but it might help beginners reading the source-code, and it helped me solidify my own understanding of the types relative to the actual code.
I made some subtle changes to the terminology and semantics:
- Programs
start
andend
- Initialization isn't optional:
- an initial
state
is required - an
init
effect is optional
- an initial
- The initial
state
is a value (not a function of a previous state - since there isn't any) - Internally, we
apply
achange
, which is an object{ state, effect? }
That last change is completely opinionated - I just don't like tuples. (Using objects is a bit more verbose, but should be less confusing to beginners, and removes the risk of getting mixed-up about which arrays in your code are tuples and which are actually arrays.)
@jonaskello I'm curious to hear what you make of this? (are you using Raj in real projects?)
@mindplay-dk I did a similar thing. I wrote something similar to raj in typescript and since then I have re-written that library several times as I needed to add more advanced stuff like GraphQL subscriptions, caching etc.
Now I've ended up with a library that is much closer to the original Elm Architecture. It uses managed effects which means commands and subscriptions that are declared as data and are handled by effect managers outside the application. This way the application can consist of only pure functions. This library currently lives inside my application but I'm hoping to find the time to open source it. So yes I'm using it in a real project, but it is quite different from raj now :-).
@jonaskello well, color me curious 🙂 ... could you put it in a sandbox somewhere?