raj icon indicating copy to clipboard operation
raj copied to clipboard

Typescript typings

Open jonaskello opened this issue 6 years ago • 12 comments

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.

jonaskello avatar Sep 02 '18 10:09 jonaskello

@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.

andrejewski avatar Sep 02 '18 17:09 andrejewski

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.

jonaskello avatar Sep 02 '18 18:09 jonaskello

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 all 0.0.x software. With raj 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 as raj-spa.

  • It's important to note that raj/runtime and raj-react/program are only used once in a full application. Perhaps having Dispatch, Effect, and Change in a raj-types package is enough to give TypeScript users enough to work with day-to-day.

andrejewski avatar Sep 03 '18 22:09 andrejewski

Thanks for taking the time to contemplate this!

Just to clarify there are three ways we could add types:

  1. Put a index.d.ts file in the root of the repo and add a types: index.d.ts field to package.json.
  2. Put a index.d.ts file in a separate package @types/raj.
  3. 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-loggers 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.

jonaskello avatar Sep 04 '18 07:09 jonaskello

So I did a PR to support this discussion in #32.

jonaskello avatar Sep 04 '18 07:09 jonaskello

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 avatar May 01 '19 08:05 pandorasNox

@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.

jonaskello avatar May 01 '19 09:05 jonaskello

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 avatar Sep 02 '19 07:09 mindplay-dk

@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.

jonaskello avatar Sep 02 '19 08:09 jonaskello

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 and end
  • Initialization isn't optional:
    • an initial state is required
    • an init effect is optional
  • The initial state is a value (not a function of a previous state - since there isn't any)
  • Internally, we apply a change, 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 avatar Jan 25 '20 13:01 mindplay-dk

@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 avatar Jan 25 '20 15:01 jonaskello

@jonaskello well, color me curious 🙂 ... could you put it in a sandbox somewhere?

mindplay-dk avatar Jan 25 '20 17:01 mindplay-dk