Suggestion on inline usage, updates, simple records and "_" prop as a default
See original GitHub issueI started to use unionize at work for modelling redux states. So far it is a great conceptual fit however we miss several features/syntax 😃
Usually it looks similar to this.
const State = unionize({
Loading: ofType<{}>(), // note there is no payload
Loaded: ofType<{ data: string }>(),
Error: ofType<{ err: string }>()
});
Pattern matching
So far we have way more inline usage of matching (when there is an object to match with right away). default syntax is a bit verbose ()=>{…} payload for default case is not the initial object (useful for updates)
Suggestion
const val = State.Loaded({ data: 'some data' });
const str = State.match(val, { // note that val is a fist arg here
Loaded: ({ data }) => data,
_: v => 'default' // default, v === val here
});
“_” is a reserved prop for default case. Which is aligned with other languages + probably won’t be used (vs “def” or “default”)
In summary
-
“_” is a fallback case + the original object is passed as payload so it is possible to write {_:identity} where identity = x=>x
-
inline usage vs curried. Maybe another func name: matchInl, matchI or even switch 😃
First class support for no payload cases
const State = unionize({
Loading: simple(), // need a better name though, "noArgs" maybe?
Loaded: ofType<{ data: string }>(),
Error: ofType<{ err: string }>()
});
const loading = State.Loading(); // note no args to create it.
//So it can even be memoized (return the same object all the time)
Immutable update
this comes up a lot with dealing with redux. I need to modify data only if state is loaded.
const updatedVal = State.update(val, {
Loaded: ({ data }) => ({ data: data + ' yay!' })
});
Note:
- update signature: update:: State -> State.
- If Loaded case would have a different shape ofType<{data: string, count: number}> nothing would change. So Loaded case has a signature of Loaded:: Loaded -> Partial Loaded (similar to react’s “setState”).
- There is no need to provide Error and Loading cases. The same value is returned if there is no match. Functionally will be similar to prism.
config object as an argument
I know that in ur example it was used to represent redux actions but writing
const Action = unionize({
ADD_TODO: ofType<{ id: string; text: string }>(),
}, 'type', 'payload');
const action = Action.ADD_TODO({...})
is a bit tedious.
suggestion
const Todos = unionize(
{ Add: ofType<{ id: string; text: string }>() },
{ tag: 'type', payload: 'payload', prefix: '[TODOS] ' }
);
type Config = { tag?: string; payload?: string; prefix?: string };
//so you can simplify creating new actions
const namespace = (prefix: string) => ({ tag: 'type', payload: 'payload', prefix })
const Todos = unionize(
{ Add: ofType<{ id: string; text: string }>() },
namespace( '[TODOS] ')
);
type AddType = typeof Todos._Record.Add
// {tag: "[TODOS] Add", payload: {id: string; text: string}}
I know this is a lot of suggestions but I decided to reach out to see if they make sense to you 😃
And thanks for an amazing library!
Issue Analytics
- State:
- Created 6 years ago
- Comments:7 (7 by maintainers)
Sorry, been a busy week! And I wanted to have time to think carefully about everything you wrote. As you said, it’s a lot of suggestions 😃
We should just overload
match
IMO. The “magic” is just checking anything you could check at runtime in JS… in this case yes, it’s sufficient to check the number of arguments, as long as the default case change (1.1) happens first.default
would be my strong preference, because it’s completely analogous to the semantics ofdefault
inswitch
statements.No, that’s been possible forever. It works because all
ofType<T>()
does is give you something (erroneously) of typeT
. But{}
by itself is already of type{}
, so you never need to writeofType<{}>()
.It would be nice and consistent for it to be overloaded in the same way as
match
.Your default case syntax (1.1) is already a breaking change, and less avoidably so, so I think we should just embrace it and make this a breaking change as well. It’s a better API, so we should just go with it and not try to maintain backwards compatibility.
Separate PRs would be preferable. As I noted you’d probably want to implement (1.1) before (1) and (3) because it makes disambiguating the overloads as simple as checking the number of arguments.
I would be open to adding an overload that lets you pass the object to be matched first. I do find lots of uses for passing the curried match function to a HOF though, so I definitely want to keep that as an option.
I guess I don’t see any harm in adding this. I usually feel like it’s a bit superfluous to pass someone an object they already clearly have in their possession, since they passed it to you, but I can see that it’s useful if you’re passing an expression to the match function.
I normally shy away from reserving words, but I think this is fine, and would make it easier to disambiguate the overloads due to the first suggestion. Although I’d probably opt for
default
instead of_
.I would just use
Loading: {}
, why does it need to be more complicated? In the case of “unit” types, there’s no need to use the fakery ofofType<...>()
, you can just provide a literal value of the type.There was a PR to accomplish this but it was deferred until 2.8 lands. Actually, with conditional types it might even be possible to make it not a function at all, just a value
State.Loading
…I really like it! 👍
Also like it!
Thanks for the suggestions, I think there are a lot of good ideas here. I’m quite busy at the moment so I can’t promise when I’ll have time to get to them, but I would certainly entertain PRs 😄