Skip to content

Commit 1370f6e

Browse files
surajsuraj
authored andcommitted
fix(core): improve resource generics and params typing
Allow single generic usage when params are not defined and prevent accessing params in loader when not provided. - add the test cases of all scenario and edge cases
1 parent 4b58de4 commit 1370f6e

3 files changed

Lines changed: 271 additions & 15 deletions

File tree

packages/core/rxjs-interop/test/rx_resource_spec.ts

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {of, Observable, BehaviorSubject, throwError} from 'rxjs';
1010
import {TestBed} from '../../testing';
1111
import {timeout} from '@angular/private/testing';
12-
import {ApplicationRef, Injector, signal} from '../../src/core';
12+
import {ApplicationRef, Injector, ResourceRef, signal} from '../../src/core';
1313
import {rxResource} from '../src';
1414

1515
describe('rxResource()', () => {
@@ -116,6 +116,163 @@ describe('rxResource()', () => {
116116
});
117117
});
118118

119+
describe('types', () => {
120+
it('should type stream params as null when params option is omitted', () => {
121+
rxResource({
122+
stream: ({params}) => {
123+
const _null: null = params;
124+
// @ts-expect-error - params is null, not string
125+
const _str: string = params;
126+
return of('');
127+
},
128+
injector: TestBed.inject(Injector),
129+
});
130+
});
131+
132+
it('should type stream params correctly when params is provided', () => {
133+
rxResource({
134+
params: () => 'foo',
135+
stream: ({params}) => {
136+
const _str: string = params;
137+
// @ts-expect-error - params is string, not null
138+
const _null: null = params;
139+
return of('');
140+
},
141+
injector: TestBed.inject(Injector),
142+
});
143+
});
144+
145+
it('should type stream params as null with explicit single generic', () => {
146+
rxResource<string>({
147+
stream: ({params}) => {
148+
const _null: null = params;
149+
// @ts-expect-error - params is null, not string
150+
const _str: string = params;
151+
return of('');
152+
},
153+
injector: TestBed.inject(Injector),
154+
});
155+
});
156+
it('should exclude undefined from stream params when params can return undefined', () => {
157+
const condition = signal(true);
158+
rxResource({
159+
params: () => (condition() ? 'foo' : undefined),
160+
stream: ({params}) => {
161+
// params should be string, not string | undefined
162+
const _str: string = params;
163+
return of('');
164+
},
165+
injector: TestBed.inject(Injector),
166+
});
167+
});
168+
it('should type stream params as nullable when params option is potentially undefined', () => {
169+
function getParams(): (() => string) | undefined {
170+
return undefined;
171+
}
172+
rxResource({
173+
params: getParams(),
174+
stream: ({params}) => {
175+
const _nullable: string | null = params;
176+
// @ts-expect-error - params could be null
177+
const _str: string = params;
178+
return of('');
179+
},
180+
injector: TestBed.inject(Injector),
181+
});
182+
});
183+
it('should type stream params as nullable with two explicit generics and no params', () => {
184+
rxResource<string, string>({
185+
stream: ({params}) => {
186+
const _nullable: string | null = params;
187+
// @ts-expect-error - params could be null since params option is not provided
188+
const _str: string = params;
189+
return of('');
190+
},
191+
injector: TestBed.inject(Injector),
192+
});
193+
});
194+
195+
it('should type stream params as nullable with two explicit generics and params: undefined', () => {
196+
rxResource<string, string>({
197+
params: undefined,
198+
stream: ({params}) => {
199+
const _nullable: string | null = params;
200+
// @ts-expect-error - params could be null since params is undefined
201+
const _str: string = params;
202+
return of('');
203+
},
204+
injector: TestBed.inject(Injector),
205+
});
206+
});
207+
208+
it('should narrow hasValue() when the value can be undefined', () => {
209+
const result: ResourceRef<number | undefined> = rxResource({
210+
params: () => 1,
211+
stream: ({params}) => of(params),
212+
injector: TestBed.inject(Injector),
213+
});
214+
if (result.hasValue()) {
215+
const _value: number = result.value();
216+
} else if (result.isLoading()) {
217+
// @ts-expect-error
218+
const _value: number = result.value();
219+
} else if (result.error()) {
220+
}
221+
const readonly = result.asReadonly();
222+
if (readonly.hasValue()) {
223+
const _value: number = readonly.value();
224+
} else if (readonly.isLoading()) {
225+
// @ts-expect-error
226+
const _value: number = readonly.value();
227+
} else if (readonly.error()) {
228+
}
229+
});
230+
231+
it('should not narrow hasValue() when a default value is provided', () => {
232+
const result: ResourceRef<number> = rxResource({
233+
params: () => 1,
234+
stream: ({params}) => of(params),
235+
injector: TestBed.inject(Injector),
236+
defaultValue: 0,
237+
});
238+
if (result.hasValue()) {
239+
const _value: number = result.value();
240+
} else if (result.isLoading()) {
241+
const _value: number = result.value();
242+
} else if (result.error()) {
243+
}
244+
const readonly = result.asReadonly();
245+
if (readonly.hasValue()) {
246+
const _value: number = readonly.value();
247+
} else if (readonly.isLoading()) {
248+
const _value: number = readonly.value();
249+
} else if (readonly.error()) {
250+
}
251+
});
252+
253+
it('should not narrow hasValue() when the resource type is unknown', () => {
254+
const result: ResourceRef<unknown> = rxResource({
255+
params: () => 1 as unknown,
256+
stream: ({params}) => of(params),
257+
injector: TestBed.inject(Injector),
258+
defaultValue: 0,
259+
});
260+
if (result.hasValue()) {
261+
const _value: unknown = result.value();
262+
} else if (result.isLoading()) {
263+
const _value: unknown = result.value();
264+
} else if (result.error()) {
265+
}
266+
const readonly = result.asReadonly();
267+
if (readonly.hasValue()) {
268+
const _value: unknown = readonly.value();
269+
} else if (readonly.isLoading()) {
270+
const _value: unknown = readonly.value();
271+
} else if (readonly.error()) {
272+
}
273+
});
274+
});
275+
119276
async function waitFor(fn: () => boolean): Promise<void> {
120277
while (!fn()) {
121278
await timeout(1);

packages/core/src/resource/resource.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -452,20 +452,13 @@ export class ResourceImpl<T, R> extends BaseWritableResource<T> implements Resou
452452
// reactive. This avoids any confusion with signals tracking or not tracking depending on
453453
// which side of the `await` they are.
454454
const stream = await untracked(() => {
455-
const requestValue = extRequest.request as R;
456-
const loaderParams = (
457-
requestValue === undefined
458-
? {
459-
abortSignal,
460-
previous: {status: previousStatus},
461-
}
462-
: {
463-
params: requestValue,
464-
abortSignal,
465-
previous: {status: previousStatus},
466-
}
467-
) as SafeLoaderParams<R>;
468-
return this.loaderFn(loaderParams);
455+
return this.loaderFn({
456+
params: extRequest.request as Exclude<R, undefined>,
457+
abortSignal,
458+
previous: {
459+
status: previousStatus,
460+
},
461+
} as ResourceLoaderParams<R>);
469462
});
470463

471464
// If this request has been aborted, or the current request no longer

packages/core/test/resource/resource_spec.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,112 @@ describe('resource', () => {
10191019
});
10201020
});
10211021

1022+
describe('types', () => {
1023+
it('should type loader params as null when params option is omitted', () => {
1024+
resource({
1025+
loader: async ({params}) => {
1026+
const _null: null = params;
1027+
// @ts-expect-error - params is null, not string
1028+
const _str: string = params;
1029+
return '';
1030+
},
1031+
injector: TestBed.inject(Injector),
1032+
});
1033+
});
1034+
1035+
it('should type loader params correctly when params is provided', () => {
1036+
resource({
1037+
params: () => 'foo',
1038+
loader: async ({params}) => {
1039+
const _str: string = params;
1040+
// @ts-expect-error - params is string, not null
1041+
const _null: null = params;
1042+
return '';
1043+
},
1044+
injector: TestBed.inject(Injector),
1045+
});
1046+
});
1047+
1048+
it('should type loader params as null with explicit single generic', () => {
1049+
resource<string>({
1050+
loader: async ({params}) => {
1051+
const _null: null = params;
1052+
// @ts-expect-error - params is null, not string
1053+
const _str: string = params;
1054+
return '';
1055+
},
1056+
injector: TestBed.inject(Injector),
1057+
});
1058+
});
1059+
1060+
it('should type loader params as null when params is explicitly undefined', () => {
1061+
resource({
1062+
params: undefined,
1063+
loader: async ({params}) => {
1064+
const _null: null = params;
1065+
// @ts-expect-error - params is null, not string
1066+
const _str: string = params;
1067+
return '';
1068+
},
1069+
injector: TestBed.inject(Injector),
1070+
});
1071+
});
1072+
1073+
it('should exclude undefined from loader params when params can return undefined', () => {
1074+
const condition = signal(true);
1075+
resource({
1076+
params: () => (condition() ? 'foo' : undefined),
1077+
loader: async ({params}) => {
1078+
// params should be string, not string | undefined
1079+
const _str: string = params;
1080+
return '';
1081+
},
1082+
injector: TestBed.inject(Injector),
1083+
});
1084+
});
1085+
1086+
it('should type loader params as nullable when params option is potentially undefined', () => {
1087+
function getParams(): (() => string) | undefined {
1088+
return undefined;
1089+
}
1090+
resource({
1091+
params: getParams(),
1092+
loader: async ({params}) => {
1093+
const _nullable: string | null = params;
1094+
// @ts-expect-error - params could be null
1095+
const _str: string = params;
1096+
return '';
1097+
},
1098+
injector: TestBed.inject(Injector),
1099+
});
1100+
});
1101+
1102+
it('should type loader params as nullable with two explicit generics and no params', () => {
1103+
resource<string, string>({
1104+
loader: async ({params}) => {
1105+
const _nullable: string | null = params;
1106+
// @ts-expect-error - params could be null since params option is not provided
1107+
const _str: string = params;
1108+
return '';
1109+
},
1110+
injector: TestBed.inject(Injector),
1111+
});
1112+
});
1113+
1114+
it('should type loader params as nullable with two explicit generics and params: undefined', () => {
1115+
resource<string, string>({
1116+
params: undefined,
1117+
loader: async ({params}) => {
1118+
const _nullable: string | null = params;
1119+
// @ts-expect-error - params could be null since params is undefined
1120+
const _str: string = params;
1121+
return '';
1122+
},
1123+
injector: TestBed.inject(Injector),
1124+
});
1125+
});
1126+
});
1127+
10221128
function flushMicrotasks(): Promise<void> {
10231129
return new Promise((resolve) => setTimeout(resolve, 0));
10241130
}

0 commit comments

Comments
 (0)