Skip to content

Commit 7d32438

Browse files
committed
fix: align helper data typings with runtime state
1 parent 6839eb1 commit 7d32438

File tree

8 files changed

+118
-11
lines changed

8 files changed

+118
-11
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export class AppComponent {
8181
}
8282
```
8383

84+
`data()` is typed as `T | undefined`. Handle the initial/skipped state with
85+
`?.` or `??` until the first successful result arrives.
86+
8487
### Mutating data
8588

8689
Use `injectMutation` to mutate the database.
@@ -104,6 +107,9 @@ export class AppComponent {
104107
}
105108
```
106109

110+
`data()` is typed as `T | undefined` and stays undefined until the first
111+
successful mutation result or after `reset()`.
112+
107113
### Running actions
108114

109115
Use `injectAction` to run actions.
@@ -125,6 +131,9 @@ export class AppComponent {
125131
}
126132
```
127133

134+
`data()` is typed as `T | undefined` and stays undefined until the first
135+
successful action result or after `reset()`.
136+
128137
### Paginated queries
129138

130139
Use `injectPaginatedQuery` for infinite scroll or "load more" patterns.

packages/convex-angular/README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export class AppComponent {
8181
}
8282
```
8383

84+
`data()` is typed as `T | undefined`. Handle the initial/skipped state with
85+
`?.` or `??` until the first successful result arrives.
86+
8487
### Mutating data
8588

8689
Use `injectMutation` to mutate the database.
@@ -93,9 +96,7 @@ import { api } from '../convex/_generated/api';
9396

9497
@Component({
9598
selector: 'app-root',
96-
template: `
97-
<button (click)="addTodoItem()">Add Todo</button>
98-
`,
99+
template: ` <button (click)="addTodoItem()">Add Todo</button> `,
99100
})
100101
export class AppComponent {
101102
readonly addTodo = injectMutation(api.todos.addTodo);
@@ -113,6 +114,9 @@ export class AppComponent {
113114
`mutate()` rejects on failure. `error()` and `status()` are still updated, and
114115
`onError` still runs before the promise rejects.
115116

117+
`data()` is typed as `T | undefined` and stays undefined until the first
118+
successful mutation result or after `reset()`.
119+
116120
### Running actions
117121

118122
Use `injectAction` to run actions.
@@ -143,6 +147,9 @@ export class AppComponent {
143147
`run()` rejects on failure. `error()` and `status()` are still updated, and
144148
`onError` still runs before the promise rejects.
145149

150+
`data()` is typed as `T | undefined` and stays undefined until the first
151+
successful action result or after `reset()`.
152+
146153
### Paginated queries
147154

148155
Use `injectPaginatedQuery` for infinite scroll or "load more" patterns.

packages/convex-angular/src/lib/providers/inject-action.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ import { FunctionReference } from 'convex/server';
66
import { CONVEX } from '../tokens/convex';
77
import { ActionReference, injectAction } from './inject-action';
88

9+
type Assert<T extends true> = T;
10+
type IsExact<T, Expected> = [T] extends [Expected]
11+
? [Expected] extends [T]
12+
? true
13+
: false
14+
: false;
15+
916
// Mock action function reference
1017
const mockAction = (() => {}) as unknown as FunctionReference<
1118
'action',
@@ -66,6 +73,29 @@ describe('injectAction', () => {
6673
expect(fixture.componentInstance.sendEmail.data()).toBeUndefined();
6774
});
6875

76+
it('should type data as action result or undefined', () => {
77+
@Component({
78+
template: '',
79+
standalone: true,
80+
})
81+
class TestComponent {
82+
readonly sendEmail = injectAction(mockAction);
83+
}
84+
85+
const fixture = TestBed.createComponent(TestComponent);
86+
fixture.detectChanges();
87+
88+
type ActionData = ReturnType<TestComponent['sendEmail']['data']>;
89+
const assertActionDataType: Assert<
90+
IsExact<ActionData, { success: boolean } | undefined>
91+
> = true;
92+
93+
const typedData: ActionData = fixture.componentInstance.sendEmail.data();
94+
95+
expect(assertActionDataType).toBe(true);
96+
expect(typedData).toBeUndefined();
97+
});
98+
6999
it('should initialize with no error', () => {
70100
@Component({
71101
template: '',

packages/convex-angular/src/lib/providers/inject-action.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export interface ActionResult<Action extends ActionReference> {
4848
* The data returned by the last successful action call.
4949
* Undefined until the action completes successfully.
5050
*/
51-
data: Signal<FunctionReturnType<Action>>;
51+
data: Signal<FunctionReturnType<Action> | undefined>;
5252

5353
/**
5454
* The error from the last failed action call.
@@ -128,7 +128,7 @@ export function injectAction<Action extends ActionReference>(
128128
);
129129

130130
// Internal signals for tracking state
131-
const data = signal<FunctionReturnType<Action>>(undefined);
131+
const data = signal<FunctionReturnType<Action> | undefined>(undefined);
132132
const error = signal<Error | undefined>(undefined);
133133
const isLoading = signal(false);
134134
const currentVersion = signal(0);

packages/convex-angular/src/lib/providers/inject-mutation.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ import { FunctionReference } from 'convex/server';
66
import { CONVEX } from '../tokens/convex';
77
import { MutationReference, injectMutation } from './inject-mutation';
88

9+
type Assert<T extends true> = T;
10+
type IsExact<T, Expected> = [T] extends [Expected]
11+
? [Expected] extends [T]
12+
? true
13+
: false
14+
: false;
15+
916
// Mock mutation function reference
1017
const mockMutation = (() => {}) as unknown as FunctionReference<
1118
'mutation',
@@ -66,6 +73,29 @@ describe('injectMutation', () => {
6673
expect(fixture.componentInstance.addTodo.data()).toBeUndefined();
6774
});
6875

76+
it('should type data as mutation result or undefined', () => {
77+
@Component({
78+
template: '',
79+
standalone: true,
80+
})
81+
class TestComponent {
82+
readonly addTodo = injectMutation(mockMutation);
83+
}
84+
85+
const fixture = TestBed.createComponent(TestComponent);
86+
fixture.detectChanges();
87+
88+
type MutationData = ReturnType<TestComponent['addTodo']['data']>;
89+
const assertMutationDataType: Assert<
90+
IsExact<MutationData, { id: string } | undefined>
91+
> = true;
92+
93+
const typedData: MutationData = fixture.componentInstance.addTodo.data();
94+
95+
expect(assertMutationDataType).toBe(true);
96+
expect(typedData).toBeUndefined();
97+
});
98+
6999
it('should initialize with no error', () => {
70100
@Component({
71101
template: '',

packages/convex-angular/src/lib/providers/inject-mutation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export interface MutationResult<Mutation extends MutationReference> {
6161
* The data returned by the last successful mutation call.
6262
* Undefined until the mutation completes successfully.
6363
*/
64-
data: Signal<FunctionReturnType<Mutation>>;
64+
data: Signal<FunctionReturnType<Mutation> | undefined>;
6565

6666
/**
6767
* The error from the last failed mutation call.
@@ -148,7 +148,7 @@ export function injectMutation<Mutation extends MutationReference>(
148148
);
149149

150150
// Internal signals for tracking state
151-
const data = signal<FunctionReturnType<Mutation>>(undefined);
151+
const data = signal<FunctionReturnType<Mutation> | undefined>(undefined);
152152
const error = signal<Error | undefined>(undefined);
153153
const isLoading = signal(false);
154154
const currentVersion = signal(0);

packages/convex-angular/src/lib/providers/inject-query.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import { skipToken } from '../skip-token';
1212
import { CONVEX } from '../tokens/convex';
1313
import { QueryReference, injectQuery } from './inject-query';
1414

15+
type Assert<T extends true> = T;
16+
type IsExact<T, Expected> = [T] extends [Expected]
17+
? [Expected] extends [T]
18+
? true
19+
: false
20+
: false;
21+
1522
// Mock getFunctionName to avoid needing a real FunctionReference
1623
jest.mock('convex/server', () => ({
1724
...jest.requireActual('convex/server'),
@@ -96,6 +103,29 @@ describe('injectQuery', () => {
96103
expect(mockConvexClient.onUpdate).toHaveBeenCalled();
97104
}));
98105

106+
it('should type data as query result or undefined', () => {
107+
@Component({
108+
template: '',
109+
standalone: true,
110+
})
111+
class TestComponent {
112+
readonly todos = injectQuery(mockQuery, () => ({ count: 10 }));
113+
}
114+
115+
const fixture = TestBed.createComponent(TestComponent);
116+
fixture.detectChanges();
117+
118+
type TodosData = ReturnType<TestComponent['todos']['data']>;
119+
const assertTodosDataType: Assert<
120+
IsExact<TodosData, Array<{ _id: string; title: string }> | undefined>
121+
> = true;
122+
123+
const typedData: TodosData = fixture.componentInstance.todos.data();
124+
125+
expect(assertTodosDataType).toBe(true);
126+
expect(typedData).toBeUndefined();
127+
});
128+
99129
it('should set isLoading to true initially', fakeAsync(() => {
100130
@Component({
101131
template: '',

packages/convex-angular/src/lib/providers/inject-query.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,11 @@ export interface QueryOptions<Query extends QueryReference> {
5454
export interface QueryResult<Query extends QueryReference> {
5555
/**
5656
* The current data from the query subscription.
57-
* Initially populated from local cache if available, then updated reactively.
58-
* Data is preserved during refetch for better UX.
57+
* Undefined until cached data or the first successful result is available.
58+
* Data is also undefined when the query is skipped.
59+
* The last successful value is preserved during refetch for better UX.
5960
*/
60-
data: Signal<FunctionReturnType<Query>>;
61+
data: Signal<FunctionReturnType<Query> | undefined>;
6162

6263
/**
6364
* The current error, if the query subscription failed.
@@ -162,7 +163,7 @@ export function injectQuery<Query extends QueryReference>(
162163
const destroyRef = inject(DestroyRef);
163164

164165
// Initialize signals
165-
const data = signal<FunctionReturnType<Query>>(undefined);
166+
const data = signal<FunctionReturnType<Query> | undefined>(undefined);
166167
const error = signal<Error | undefined>(undefined);
167168
const isLoading = signal(false);
168169
const isSkipped = signal(false);

0 commit comments

Comments
 (0)