-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
feat: improve helper types for more type safety #1121
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
Closed
Closed
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
0768c80
feat: improve helper types to utilize Vue 2.5 types
ktsn 25775c5
feat(types): [WIP] more strict typed store assets
ktsn 21ee399
Merge branch 'dev' into feat-improve-typing
ktsn eee1f3c
fix(types): relax map state function type
ktsn f94cf70
test(types): update namespaced helper type test
ktsn a5c4e26
feat(types): allow to specify assets types on mapXXX helpers
ktsn 58d28a5
chore(types): add comments for helpers and utilities types
ktsn 0858c6d
fix(types): revert renaming Payload to avoid breaking change
ktsn 1e27c5e
feat(helpers): return root helpers if no namespace is provided to cre…
ktsn c2068f3
feat(types): add `DefineModule` utility type
ktsn 7abf34f
fix(types): allow to omit payload on mapped methods if it is untyped
ktsn 9564b80
docs(helpers): improve `createNamespacedHelpers` description
ktsn 1aa407f
fix(types): expose DefineGetters/Mutations/Actions type
ktsn cfb6042
chore: include utils.d.ts for `files` field
ktsn 00360b5
fix(types): make dispatch/commit more type safe in module actions if …
ktsn 9b89ae7
fix(types): remove default type parameters from StrictDispatch/Commit
ktsn 09475d5
refactor: use undefined type to indicate empty payload instead of null
ktsn 8e0c60b
fix(types): fix incorrect type annotation
ktsn b14662c
fix(types): fix ActionContext type
ktsn b24d744
fix(types): remove incorrect overload
ktsn 8b6a6f9
refactor(types): just use Dispatch/CommitOptions instead of BaseDispa…
ktsn 9b183f0
docs: add typescript docs
ktsn 715eaad
docs: fix typo in typescript docs
ktsn fc1f29b
docs: add a note about conditional types
ktsn c3626f7
Merge branch 'dev' into feat-improve-typing
ktsn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
# TypeScript Support | ||
|
||
## Utility Types for Modules | ||
|
||
Vuex provides some utility types to help you to declare modules in TypeScript. They avoid runtime errors when using state, getters, mutations and actions in a module thanks to type checking. | ||
|
||
To use the utility types, you should declare module assets types at first. Following is a simple example of counter module types: | ||
|
||
```ts | ||
// State type | ||
export interface CounterState { | ||
count: number | ||
} | ||
|
||
// Getters type | ||
// key: getter name | ||
// value: return type of getter | ||
export interface CounterGetters { | ||
power: number | ||
} | ||
|
||
// Mutations type | ||
// key: mutation name | ||
// value: payload type of mutation | ||
export interface CounterMutations { | ||
increment: { amount: number } | ||
} | ||
|
||
// Actions type | ||
// key: action name | ||
// value: payload type of action | ||
export interface CounterActions { | ||
incrementAsync: { amount: number, delay: number } | ||
} | ||
``` | ||
|
||
The state type must describe an actual state shape. The `CounterState` in the example indicates that the module's state has `count` property which must fulfill `number` type. | ||
|
||
The getters type describes what getter names exist in the module according to keys. The corresponding value type shows what type the getter returns. The `CounterGetters` in the example indicates that the module has a getter named `power` and it returns a value of type `number`. | ||
|
||
Both the actions and mutations type describe what thier names exist in the module as same as getters type. The value type of them indicates the payload type. The `CounterMutations` illustrates that the module has `increment` mutation and its payload is an object having `amount` property of type `number`, while the `CounterActions` shows there is `incrementAsync` action with an object payload having `amount` and `delay` property of type `number` in the module. | ||
|
||
After declaring the module assets types, you import `DefineModule` utility type and annotate the module with it: | ||
|
||
```ts | ||
import { DefineModule } from 'vuex' | ||
|
||
// Implementation of counter module | ||
export const counter: DefineModule<CounterState, CounterGetters, CounterMutations, CounterActions> = { | ||
namespaced: true, | ||
|
||
// Follow CounterState | ||
state: { | ||
count: 0 | ||
}, | ||
|
||
// Follow CounterGetters | ||
getters: { | ||
power: state => state.count * state.count | ||
}, | ||
|
||
// Follow CounterMutations | ||
mutations: { | ||
increment (state, payload) { | ||
state.count += payload.amount | ||
} | ||
}, | ||
|
||
// Follow CounterActions | ||
actions: { | ||
incrementAsync ({ commit }, payload) { | ||
setTimeout(() => { | ||
commit('increment', { amount: payload.amount }) | ||
}, payload.delay) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Note that all function arguments types are inferred without manually annotating them including `dispatch` and `commit` in the action context. If you try to dispach an action (commit a mutation) that does not exist or the payload type is not valid on the declared types, it throws a compilation error. | ||
|
||
### Using external modules in the same namespace | ||
|
||
Sometimes you may want to use external modules' getters, actions and mutations in the same namespace. In that case, you can pass the external module assets types to `DefineModule` generic parameters to extend the module type: | ||
|
||
```ts | ||
// External module assets types | ||
// You may import them from another file on a practical code | ||
interface ExternalGetters { | ||
extraValue: number | ||
} | ||
|
||
interface ExternalMutations { | ||
loading: boolean | ||
} | ||
|
||
interface ExternalActions { | ||
sendTrackingData: { name: string, value: string } | ||
} | ||
|
||
export const counter: DefineModule< | ||
// The first 4 type parameters are for module assets | ||
CounterState, | ||
CounterGetters, | ||
CounterMutations, | ||
CounterActions, | ||
|
||
// 3 type parameters that follows the module assets types are external module assets types | ||
ExternalGetters, | ||
ExternalMutations, | ||
ExternalActions | ||
> = { | ||
namespaced: true, | ||
|
||
state: { /* ... */ }, | ||
mutations: { /* ... */ }, | ||
|
||
getters: { | ||
power (state, getters) { | ||
// You can use a getter from the external module | ||
console.log(getters.extraValue) | ||
return state.count * state.count | ||
} | ||
}, | ||
|
||
actions: { | ||
incrementAsync ({ commit, dispatch }, payload) { | ||
// Using the external action | ||
dispatch('sendTrackingData', { | ||
name: 'increment', | ||
value: payload.amount | ||
}) | ||
|
||
// Using the external mutation | ||
commit('loading', true) | ||
setTimeout(() => { | ||
commit('increment', { amount: payload.amount }) | ||
commit('loading', false) | ||
}, payload.delay) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
### Using the root state, getters, actions and mutations | ||
|
||
If you want to use root state, getters, actions and mutations, you can pass root assets types following external assets types on `DefineModule`: | ||
|
||
```ts | ||
export const counter: DefineModule< | ||
CounterState, | ||
CounterGetters, | ||
CounterMutations, | ||
CounterActions, | ||
|
||
// You can use `{}` type if you will not use them | ||
{}, // External getters | ||
{}, // External mutations | ||
{}, // External actions | ||
|
||
// Root types can be specified after external assets types | ||
RootState, | ||
RootGetters, | ||
RootMutations, | ||
RootActions | ||
> = { | ||
/* ... module implementation ... */ | ||
} | ||
``` | ||
|
||
## Typed Component Binding Helpers | ||
|
||
You probably want to use fully typed `state`, `getters`, `dispatch` and `commit` not only in modules but also from components. You can use `createNamespacedHelpers` to use typed module assets on components. The `createNamespacedHelpers` accepts 4 generic parameters to annotate returned `mapState`, `mapGetters`, `mapMutations` and `mapActions` by using module assets types: | ||
|
||
```ts | ||
export const counterHelpers = createNamespacedHelpers<CounterState, CounterGetters, CounterMutations, CounterActions>('counter') | ||
``` | ||
|
||
All the returned helpers and mapped computed properties and methods will be type checked. You can use them without concerning typos and invalid payload by yourself: | ||
|
||
```ts | ||
export default Vue.extend({ | ||
computed: counterHelpers.mapState({ | ||
value: 'count' | ||
}), | ||
|
||
methods: counterHelpers.mapMutations({ | ||
inc: 'increment' | ||
}), | ||
|
||
created () { | ||
// These are correctly typed! | ||
this.inc({ amount: 1 }) | ||
console.log(this.value) | ||
} | ||
}) | ||
``` | ||
|
||
### Annotating Root Binding Helpers | ||
|
||
`createNamespacedHelpers` is made for generating new component binding helpers focusing a namespaced module. The API however is useful to create typed root binding helpers. So if you need them, you call `createNamespacedHelpers` without passing namespace: | ||
|
||
```ts | ||
const rootHelpers = createNamespacedHelpers<RootState, RootGetters, RootMutations, RootActions>() | ||
``` | ||
|
||
## Explicit Payload | ||
|
||
While regular (not strictly typed) `dispatch` and `commit` can omit a payload, typed ones does not allow to omit it. This is because to ensure type safety of a payload. If you want to declare actions / mutations that do not have a payload you should explicitly pass `undefined` value. | ||
|
||
```ts | ||
export interface CounterMutation { | ||
// This indicates the `increment` action does not have a payload | ||
increment: undefined | ||
} | ||
|
||
// ... | ||
export const counter: DefineModule<CounterState, CounterGetters, CounterMutations, CounterActions> = { | ||
// ... | ||
|
||
actions: { | ||
someAction ({ commit }) { | ||
// Passing `undefined` value explicitly | ||
commit('increment', undefined) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
This restriction might be changed after TypeScript implements [the conditional types](https://github.com/Microsoft/TypeScript/pull/21316) in the future. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
We can add a note telling users this might change in future. microsoft/TypeScript#21316
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'll add it later.