Skip to content

Commit be0ed64

Browse files
author
Sascha Braun
committed
add optional parameter handling and getGetters utils for getters type inference
1 parent a2d51c7 commit be0ed64

File tree

7 files changed

+104
-49
lines changed

7 files changed

+104
-49
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,33 @@ export const actions = {
8080

8181
```
8282

83+
## Circular referencing
84+
85+
**Warning**: Be careful when returning values from your **actions** and **getters**!
86+
```ts
87+
export const actions = {
88+
async incrementAsync(context) {
89+
const counterModule = Counter.getInstance(context);
90+
counterModule.commit.increment();
91+
// Circular referencing here, as incrementAsync needs the type from counterModule and counterModule needs the type from incrementAsync
92+
// Result: counterModule is cast to any
93+
return counterModule.state.count;
94+
}
95+
};
96+
```
97+
To avoid this, always manually type your return types:
98+
```ts
99+
export const actions = {
100+
// specify the return type here
101+
async incrementAsync(context): number {
102+
const counterModule = Counter.getInstance(context);
103+
counterModule.commit.increment();
104+
// everything is fine with our counterModule now
105+
return counterModule.state.count;
106+
}
107+
};
108+
```
109+
83110
## Contributors
84111

85112
If you are interested and want to help out, don't hesitate to contact me or to create a pull request with your fixes / features.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vuex-context",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"description": "Write fully type inferred Vuex modules",
55
"main": "dist/index.common.js",
66
"module": "dist/index.esm.js",
@@ -14,7 +14,7 @@
1414
"vuex",
1515
"typescript",
1616
"type inference",
17-
"namespace"
17+
"context"
1818
],
1919
"scripts": {
2020
"serve": "vue-cli-service serve ./samples/main.ts",

samples/store/counter.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
1-
import { Context } from '../../src';
1+
import { Context } from "../../src";
22

3-
export interface CounterState {
4-
count: number;
5-
}
3+
export type CounterState = ReturnType<typeof state>;
64

75
export const namespaced = true;
86

9-
export const state = (): CounterState => ({
10-
count: 0
7+
export const state = () => ({
8+
count: 0,
9+
test: 0
1110
});
1211

1312
export const mutations = {
14-
increment(state: CounterState) {
15-
state.count++;
13+
increment(state: CounterState, payload: number = 1) {
14+
state.count += payload;
1615
},
1716
incrementBy(state: CounterState, payload: number) {
1817
state.count += payload;
1918
}
2019
};
2120

2221
export const actions = {
23-
async incrementAsync(context) {
22+
async incrementAsync(context): Promise<number> {
2423
const ctx = Counter.getInstance(context);
2524
ctx.commit.increment();
25+
ctx.commit.incrementBy(12);
26+
return ctx.state.count;
2627
}
2728
};
2829

2930
export const getters = {
30-
doubleCount(state: CounterState) {
31+
doubleCount(state: CounterState): number {
3132
return state.count * 2;
33+
},
34+
quadrupleCount(state: CounterState, context): number {
35+
const getters = Counter.getGetters(context);
36+
return getters.doubleCount * 2;
3237
}
3338
};
3439

samples/store/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import Vue from 'vue';
2-
import Vuex, { Store, StoreOptions } from 'vuex';
1+
import Vue from "vue";
2+
import Vuex, { Store, StoreOptions } from "vuex";
33

4-
import * as todo from './todo';
4+
import * as counter from "./counter";
5+
import * as todo from "./todo";
56

67
Vue.use(Vuex);
78

89
const options: StoreOptions<any> = {
910
modules: {
10-
todo
11+
todo,
12+
counter
1113
}
1214
};
1315

samples/store/todo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Context } from '../../src';
1+
import { Context } from "../../src";
22

33
export interface Todo {
44
id: string;

src/index.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,59 @@
11
import {
22
ActionContext, ActionTree, CommitOptions, DispatchOptions, GetterTree, MutationTree, Store
3-
} from 'vuex';
3+
} from "vuex";
4+
5+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((
6+
k: infer I
7+
) => void)
8+
? I
9+
: never;
410

511
type InferPayload<T> = T extends (_: any, payload?: any, options?: any) => any
612
? Parameters<T>[1]
713
: never;
814

9-
type InferMutation<T> = T extends undefined | null
15+
type Mutation<T> = T extends undefined
1016
? (() => void) & ((payload: undefined, options?: CommitOptions) => void)
1117
: (payload: T, options?: CommitOptions) => void;
12-
type InferMutations<T> = { [Key in keyof T]: InferMutation<InferPayload<T[Key]>> };
1318

14-
type InferAction<T, R> = T extends undefined | null
19+
type InferMutation<T> = UnionToIntersection<Mutation<InferPayload<T>>>;
20+
type InferMutations<T> = { [Key in keyof T]: InferMutation<T[Key]> };
21+
22+
type Action<T, R> = T extends undefined
1523
? (() => R) & ((payload: undefined, options?: DispatchOptions) => R)
1624
: (payload: T, options?: DispatchOptions) => R;
25+
type ActionReturn<T> = T extends (...args: any[]) => infer R ? R : void;
1726

18-
type InferActionReturn<T> = T extends (...args: any[]) => any ? ReturnType<T> : void;
27+
type InferAction<T> = UnionToIntersection<Action<InferPayload<T>, ActionReturn<T>>>;
28+
type InferActions<T> = { [Key in keyof T]: InferAction<T[Key]> };
1929

20-
type InferActions<T> = {
21-
[Key in keyof T]: InferAction<InferPayload<T[Key]>, InferActionReturn<T[Key]>>
30+
type InferGetters<T> = {
31+
[Key in keyof T]: T[Key] extends (state: any, getters: any) => infer R ? R : never
2232
};
2333

24-
type InferGetters<T> = { [Key in keyof T]: T extends any ? ReturnType<T[Key]> : never };
34+
type InferState<S> = S extends () => infer S ? S : S;
2535

2636
export function Context<
2737
S extends (() => object) | object = {},
2838
M extends MutationTree<any> = {},
2939
A extends ActionTree<any, any> = {},
3040
G extends GetterTree<any, any> = {}
3141
>() {
32-
type State = S extends () => object ? ReturnType<S> : S;
33-
34-
const InstanceType = (null as unknown) as Readonly<{
35-
state: State;
42+
type InstanceType = Readonly<{
43+
state: InferState<S>;
3644
commit: InferMutations<M>;
3745
dispatch: InferActions<A>;
3846
getters: InferGetters<G>;
3947
}>;
4048

4149
return {
42-
InstanceType,
50+
InstanceType: (undefined as unknown) as InstanceType,
51+
52+
getGetters(getters: any): InstanceType['getters'] {
53+
return getters;
54+
},
4355

44-
getInstance(store: Store<any> | ActionContext<any, any>, ns: string = ''): typeof InstanceType {
56+
getInstance(store: Store<any> | ActionContext<any, any>, ns: string = ''): InstanceType {
4557
const splitNs = ns ? ns.split('/').filter(val => !!val) : [];
4658
const fixedNs = splitNs.length ? splitNs.join('/') + '/' : '';
4759

tests/unit/simple.spec.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,64 @@
1-
import Vue from 'vue';
2-
import Vuex from 'vuex';
1+
import Vue from "vue";
2+
import Vuex from "vuex";
33

4-
import { createStore } from '../../samples/store/';
5-
import { Todo } from '../../samples/store/todo';
4+
import { createStore } from "../../samples/store/";
5+
import { Counter } from "../../samples/store/counter";
6+
import { Todo } from "../../samples/store/todo";
67

78
Vue.use(Vuex);
89

910
describe('Simple Todo tests', () => {
1011
let store;
11-
let proxy: typeof Todo.InstanceType;
12+
let todo: typeof Todo.InstanceType;
13+
let counter: typeof Counter.InstanceType;
1214

1315
beforeEach(() => {
1416
store = createStore();
15-
proxy = Todo.getInstance(store, 'todo');
17+
todo = Todo.getInstance(store, 'todo');
18+
counter = Counter.getInstance(store, 'counter');
1619
});
1720

1821
it('Add todo', () => {
19-
expect(Array.isArray(proxy.state.list));
20-
expect(proxy.state.list.length === 0);
22+
expect(Array.isArray(todo.state.list));
23+
expect(todo.state.list.length === 0);
2124

22-
expect(typeof proxy.commit.addTodo === 'function');
25+
expect(typeof todo.commit.addTodo === 'function');
2326

24-
proxy.commit.addTodo({
27+
todo.commit.addTodo({
2528
done: false,
2629
id: 'random',
2730
text: 'Random todo'
2831
});
2932

30-
expect(proxy.state.list.length === 1);
33+
expect(todo.state.list.length === 1);
3134
});
3235

3336
it('Fetch todos', async () => {
34-
expect(Array.isArray(proxy.state.list));
35-
expect(proxy.state.list.length === 0);
37+
expect(Array.isArray(todo.state.list));
38+
expect(todo.state.list.length === 0);
3639

37-
expect(typeof proxy.dispatch.fetchTodos === 'function');
40+
expect(typeof todo.dispatch.fetchTodos === 'function');
3841

39-
const todos = await proxy.dispatch.fetchTodos();
42+
const todos = await todo.dispatch.fetchTodos();
4043
expect(Array.isArray(todos));
4144
});
4245

4346
it('Destructure mutations', async () => {
44-
const { importTodo, addTodo, toggleTodo } = proxy.commit;
47+
const { importTodo, addTodo, toggleTodo } = todo.commit;
4548

4649
expect(typeof importTodo === 'function');
4750
expect(typeof addTodo === 'function');
4851
expect(typeof toggleTodo === 'function');
4952
});
5053

54+
it('Optional payload', async () => {
55+
counter.commit.increment();
56+
counter.commit.increment(12);
57+
expect(true);
58+
});
59+
5160
it('Clear filter', () => {
52-
proxy.commit.clearFilter();
53-
expect(proxy.state.filter === '');
61+
todo.commit.clearFilter();
62+
expect(todo.state.filter === '');
5463
});
5564
});

0 commit comments

Comments
 (0)