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
Refactor typings to allow for better intellisense and support in Typescript #551
Conversation
…n value from the actions
Codecov Report
@@ Coverage Diff @@
## master #551 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 1 1
Lines 133 132 -1
Branches 40 40
=====================================
- Hits 133 132 -1
Continue to review full report at Codecov.
|
test/ts/index.tsx
Outdated
</main> | ||
) | ||
|
||
app<Counter.State, Counter.Actions>( | ||
const appActions = app<Counter.State, Counter.Actions>( |
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.
main
test/ts/index.tsx
Outdated
Counter.state, | ||
Counter.actions, | ||
view, | ||
document.body | ||
) | ||
|
||
appActions.up() |
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.
main.up()
test/ts/index.tsx
Outdated
@@ -1,38 +1,49 @@ | |||
import { h, app, ActionsType, View } from "hyperapp" | |||
import { h, app, View, StateType, UnwiredActions, UnwiredAction } from 'hyperapp' |
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 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.
Fixed
count: number | ||
clickCount: number |
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 you are going to use clickCount
, then maybe change the counter example as well, because count
and clickCount
is confusing.
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 should I change it to?
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 don't know, maybe come up with a different example. @Mytrill had a todo list originally, but I thought it was too long, so I replaced it with the current counter example.
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 put it there so we can call this action in the up
and down
action to demonstrate that the types are working properly.
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 it's not a new example, then maybe just refactor the names.
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.
IMHO, the test should have an example of asynchronous action (i.e. returning a promise) and an action where the return type is actually used by the view, since these could potentially be shortcomings of these types, which were not there with the previous version.
down(): State | ||
up(value: number): State | ||
export interface Actions extends UnwiredActions<State, Actions> { | ||
down: UnwiredAction<State, Actions> |
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.
UnwiredActions is rather long. What's wrong with ActionsType or ActionsTemplate?
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.
Nothing, just wanted to put something that's not even remotely confusing
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 for changing UnwiredAction to ActionsTemplate. But what about WiredActions? It's an internal type that no-one should be using anyway.
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 see. Don't change it yet, please.
TActions extends UnwiredActions<TState, TActions>, | ||
TPayload extends Primitive | NestedMap<Primitive> = any | ||
> = ( | ||
data?: TPayload |
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 is TPayload?
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 generic to type the payload
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 is the payload?
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 payload passed to an action when calling from the view or inside an action.
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.
Redux also calls it payload and it's called payload in the Flux architecture. I believe it's wise to keep it the same as the rest of the community calls 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.
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.
Yes the Data, but I think payload makes sense since it is used as a name in Redux too.
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.
Yes that's correct.
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 agree with this! Thanks for explaining.
Do you have any links that I can start reading to help me understand this PR? I don't expect to understand everything about TS in one sit, but at least something that can help figure out what's going on here. |
Start reading advanced types Also, to see the power of typings, open the |
@alber70g Any way we can simplify this? |
@jorgebucaran Why and what would need to be simplified? The reason it's complex is because we want to pass a type that defines the actions as UnwiredActions but want to use them as WiredActions. const actions = {
up: () => (state, actions) => ({})
} I'm talking about |
@alber70g What would make the types simpler? It would have been nice we had this convo before the 1.0. Do you find the current API particularly hard to type? or would it be just as hard if we had a different actions signature?
Because simpler is better than hard of course! :) |
@jorgebucaran I tried to change stuff before the 1.0 release but there was no attention for the typings at all. No one except me uses Hyperapp with TypeScript right now. To make things less complex, make Hyperapp simpler. The current typings represent the API of Hyperapp. Also, the definition might be complex, but using them is easy as you can see in the Ping @lassecapel: have a look at the typings, see if they're good for your redux devtools implementation |
actions.addClickCount() | ||
return { count: state.count + value } | ||
}, | ||
addClickCount: () => state => ({ clickCount: state.clickCount + 1 }), | ||
} | ||
} | ||
|
||
const view: View<Counter.State, Counter.Actions> = (state, actions) => ( |
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 View<...>
type accepts WiredActions<...>
, but here you pass it Actions
which are of type UnwiredActions<...>
, so I have 2 questions:
- does this actually compiles?
- inside the view, the user will call an unwiredActions as if it was a wired action? Shouldn't the type of View be:
export interface View<
TState extends StateType,
TActions extends WiredActions<TState, TActions>
> {
// Use the proxy here
(state: TState, actions: ActionProxy<TState, TActions>): VNode<any>
}
Other than this, neat changes! With the new API it makes sense now for users to type the unwired actions and get the wired action type auto generated!
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're right, it's a bit strange. But it does compile somehow... I'll investigate more :) Thanks for your review 👍
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're right. I've changed the signature of the View. But in your snippet the TActions extends WiredActions<..., ...>
should be TActions extends UnwiredActions<..., ...>
because you can only proxy UnwiredActions -> WiredActions. To see that it's correct now, you can look at the index.tsx and see what signature the view's onclick={() => actions.up(5)}
: (property) up: (data?: any) => Counter.State
. This is correct.
The reason why it compiles is because the UnwiredActions' TActions generic is ambiguous because it's being proxied to WiredActions :')
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.
Ah, nice!
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 like the more explicit typings especially the difference in wired and unwired actions types. The names could be discussed.
TActions extends UnwiredActions<TState, TActions>, | ||
TPayload extends Primitive | NestedMap<Primitive> = any | ||
> = ( | ||
data?: TPayload |
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.
Yes the Data, but I think payload makes sense since it is used as a name in Redux too.
/** | ||
* Convenience type for NestedMap<WiredAction> | ||
*/ | ||
interface WiredActions< |
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.
Since you implement this. It would be nice to use Actions
here. You can then use ActionsTemplate
then for the UnwiredActions
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.
Another proposal: Actions
and AppActions
. Where Actions
are the unwired ones and AppActions
the wired ones
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.
It would be nice to use Actions
for the most common one, yeah.
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 is 'most common' where you implement actions, or where you use them (e.g. in actions and in the view function)?
/** | ||
* The interface for app(). Use this for implementing Higher Order App's | ||
*/ | ||
export interface App { |
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.
This would work for the redux-devtools HOA
) => | ||
| ((state: State, actions: Actions) => ActionResult<State>) | ||
| ActionResult<State> | ||
type ActionProxy< |
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.
Nice way to define the wiring. This was causing me some trouble understanding this out of the documentation.
@jorgebucaran (& @Mytrill) I've found something that can be simplified in a way that you don't need to type the object containing the UnwiredActions, like so:
Edit: type UnwiredActions<
TState extends StateType,
TActions extends UnwiredActions<TState, TActions>
> = { [P in keyof TActions]: UnwiredAction<TState, TActions> } I'm still in a thinking process, but I thought I could share it. I'll update the PR when I'm ready. |
It's still not perfect. You cannot add nested actions in the actions object. This disallows the use of slices. Also we need to figure out a way to declare nested types and infer their type. Not sure if that's possible right now, but we can provide a 'helper' that allows for a certain level of nesting: microsoft/TypeScript#10693 Although this is possible, it's far from perfect since the View doesn't know about nested actions now (in the current state)... export interface Actions {
down: UnwiredAction<State, Actions>
up: UnwiredAction<State, Actions, number>
nested: {
addClickCount: UnwiredAction<State['nested'], Actions>
}
} But that can be fixed by this change: type ActionProxy<
TState extends StateType,
TActions extends UnwiredActions<TState, TActions>
> = { [P in keyof TActions]: TActions[P] } type ActionProxy<..., ...,> = { [P in keyof TActions]: WiredAction<TState, any> } |
I'm a bit stuck right now. The problem I face is that whenever you create a nested state and nested action, the type of the nested UnwiredAction has a state and actions passed living on the same path as the UnwiredAction. I don't know how to define that state... somehow, in the types, there needs to be a connection between state and actions. This isn't possible with two generics. If anyone would like to show me some suggestions, I might be able to integrate it. |
I used something like this before, and it seemed to work: type ActionProxy<TState extends Partial<Record<keyof TActions, any>>, TActions> = {
[P in keyof TActions]: WiredAction<TState[P], TActions[P]>
} The Hope that helps :) |
Sorry for the radio silence the past few weeks. I've been working on fixing this problem on and off, and in the mean time I've been ill. This issue of the nested types can be fixed for good once conditional types (microsoft/TypeScript#21316) are landing in TypeScript. This solution (microsoft/TypeScript#12424 (comment)) can be used in the mean time. I haven't started experimenting with it yet, but I'll try that this weekend. The final way to solve this issue is to not type it (actions) at all, and just use any. This means however, that we cannot have inferred typings for the view and the actions parameter in the actions, unless you pass them explicitly as a generic. |
@alber70g This is great work. Any plans to continue this PR. 🤔 |
Right now it can be used as is, but it'll not be flawless with slices. I'd like to wait until conditional types land in Typescript 2.8 which will be released soon I think. So for the time being, let's leave this PR open so I can continue on this one afterwards |
I've done some digging with typescript@2.8 alpha, and its not working in a way I want it to be. There's no way to infer types and use the children of a different nested type (i.e. how slices work in HA) in another type. Example (inline comments): app({ counter: 0, nested: { counter: 0} },
{
inc: () => (state) => ({ counter: state.counter + 1 }),
nested: {
inc: () => (nestedState) => ({ counter: nestedState.counter + 1 })
// currently the typings do not support Typescript in figuring out
// that this "nestedState" is the "state.nested".
}
} I will finalise this pull request in the coming week. |
Is this also true in TS 2.8? |
@jorgebucaran Yes
You cannot use a second type as a path in the first type. |
😢 |
With 2.0 we won't need most of this work, so I am going to close here. We'll need types though! But I imagine you'll prefer to create a new PR for that then? 😅 Closing. |
That's fine 👌 Once the API of 2.0 is clear I can start working on that one |
As a continuation of this PR (#535 that might be gone...) I've created this PR.
!! Please have a look at this, anyone who has interest in Typings !!
I've changed the typings considerably and I need to polish them. In the mean time it would be nice to review them for what there is now.
A list of the changes:
Things to do:
App
interfaceany
as defaults for generics)UnwiredAction
,WiredAction
. I'm wondering if we should keep it this way. I feel that it gives good distinction between the two, but others think otherwise.[discussion] a lot of generics are now typed optional withany
, it might be better to have them non-optional