Skip to content

Commit 2c78d6c

Browse files
committed
fix(forms): correct validateAsync params parameter type inference
TypeScript's type inference for AsyncValidatorOptions was locking TParams to a non-nullable type based on the factory function's Signal<TParams | undefined> parameter (a higher-priority inference site), which conflicted with the params function, whose declared return type was TParams (not TParams | undefined). As a result, returning undefined from params to skip a validation run - the documented and supported way to opt out of running the async validator for a given state - was a type error. This widens params to return TParams | undefined so its declared type matches what it always could (and needs to) return at runtime, aligning both inference sites and letting TParams be inferred consistently. Fixes angular#67859
1 parent 5ad8231 commit 2c78d6c

3 files changed

Lines changed: 48 additions & 3 deletions

File tree

goldens/public-api/forms/signals/index.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export interface AsyncValidatorOptions<TValue, TParams, TResult, TPathKind exten
4949
readonly factory: (params: Signal<TParams | undefined>) => Resource<TResult | undefined>;
5050
readonly onError: (error: unknown, ctx: FieldContext<TValue, TPathKind>) => TreeValidationResult;
5151
readonly onSuccess: MapToErrorsFn<TValue, TResult, TPathKind>;
52-
readonly params: (ctx: FieldContext<TValue, TPathKind>) => TParams;
52+
readonly params: (ctx: FieldContext<TValue, TPathKind>) => TParams | undefined;
5353
readonly when?: NoInfer<LogicFn<TValue, boolean, TPathKind>>;
5454
}
5555

packages/forms/signals/src/api/rules/validation/validate_async.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,13 @@ export interface AsyncValidatorOptions<
6565
> {
6666
/**
6767
* A function that receives the field context and returns the params for the resource.
68+
* Returning `undefined` indicates that the resource should not be run for the current state
69+
* (the forms system will report the params as `undefined` to the factory's resource).
6870
*
6971
* @param ctx The field context for the field being validated.
70-
* @returns The params for the resource.
72+
* @returns The params for the resource, or `undefined` to skip running the resource.
7173
*/
72-
readonly params: (ctx: FieldContext<TValue, TPathKind>) => TParams;
74+
readonly params: (ctx: FieldContext<TValue, TPathKind>) => TParams | undefined;
7375

7476
/**
7577
* Duration in milliseconds to wait before triggering the async operation, or a function that

packages/forms/signals/test/node/resource.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,49 @@ describe('resources', () => {
238238
expect(f[1]().errors()).toEqual([]);
239239
});
240240

241+
it('should skip async validation when params function returns undefined', async () => {
242+
let factoryCallCount = 0;
243+
const value = signal('');
244+
245+
const f = form(
246+
value,
247+
(p) => {
248+
validateAsync(p, {
249+
params: ({value}) => {
250+
const v = value().trim();
251+
return v ? v : undefined;
252+
},
253+
factory: (params) => {
254+
factoryCallCount++;
255+
return resource({
256+
params,
257+
loader: async ({params}) => params !== undefined,
258+
});
259+
},
260+
onSuccess: (exists) => (exists ? {kind: 'value-exists'} : undefined),
261+
onError: () => null,
262+
});
263+
},
264+
{injector},
265+
);
266+
267+
// empty value: params return undefined, resource should be idle( not pending)
268+
await appRef.whenStable();
269+
expect(f().pending()).toBe(false);
270+
expect(f().errors()).toEqual([]);
271+
272+
// non-empty value: params returns a string, resource should run
273+
value.set('hello');
274+
await appRef.whenStable();
275+
expect(f().errors()).toEqual([{kind: 'value-exists', fieldTree: f} as any]);
276+
277+
//back to empty: resource should go idle again, no errors
278+
value.set('');
279+
await appRef.whenStable();
280+
expect(f().errors()).toEqual([]);
281+
expect(f().pending()).toBe(false);
282+
});
283+
241284
it('should support shorthand http validation', async () => {
242285
const usernameForm = form(
243286
signal('unique-user'),

0 commit comments

Comments
 (0)