Skip to content

Commit a5b9c74

Browse files
committed
fix: validation processor types
1 parent 7444bf5 commit a5b9c74

File tree

2 files changed

+81
-5
lines changed

2 files changed

+81
-5
lines changed

api/src/core/utils/validation/validation-processor.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,5 +372,66 @@ describe('ValidationProcessor', () => {
372372
const errorCount = Object.keys(result.errors).length;
373373
expect(errorCount).toBe(25);
374374
});
375+
376+
it('should handle validation by sum typing their inputs', () => {
377+
const processor = createValidationProcessor({
378+
steps: [
379+
{
380+
name: 'step1',
381+
validator: ({ age }: { age: number }) => age > 18,
382+
isError: ResultInterpreters.booleanMeansSuccess,
383+
},
384+
{
385+
name: 'step2',
386+
validator: ({ name }: { name: string }) => name.length > 0,
387+
isError: ResultInterpreters.booleanMeansSuccess,
388+
},
389+
],
390+
});
391+
392+
const result = processor({ age: 25, name: 'John' });
393+
expect(result.isValid).toBe(true);
394+
395+
const result2 = processor({ age: 15, name: '' });
396+
expect(result2.isValid).toBe(false);
397+
});
398+
399+
it('should allow wider types as processor inputs', () => {
400+
const sumProcessor = createValidationProcessor({
401+
steps: [
402+
{
403+
name: 'step1',
404+
validator: ({ age }: { age: number }) => age > 18,
405+
isError: ResultInterpreters.booleanMeansSuccess,
406+
},
407+
{
408+
name: 'step2',
409+
validator: ({ name }: { name: string }) => name.length > 0,
410+
isError: ResultInterpreters.booleanMeansSuccess,
411+
},
412+
],
413+
});
414+
type Person = { age: number; name: string };
415+
const groupProcessor = createValidationProcessor({
416+
steps: [
417+
{
418+
name: 'step1',
419+
validator: ({ age }: Person) => age > 18,
420+
isError: ResultInterpreters.booleanMeansSuccess,
421+
},
422+
{
423+
name: 'step2',
424+
validator: ({ name }: Person) => name.length > 0,
425+
isError: ResultInterpreters.booleanMeansSuccess,
426+
},
427+
],
428+
});
429+
430+
const result = sumProcessor({ age: 25, name: 'John', favoriteColor: 'red' });
431+
expect(result.isValid).toBe(true);
432+
433+
const result2 = groupProcessor({ name: '', favoriteColor: 'red', age: 15 });
434+
expect(result2.isValid).toBe(false);
435+
});
375436
});
376437
});

api/src/core/utils/validation/validation-processor.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,18 @@ export type ValidationResult<TSteps extends readonly ValidationStepConfig<any, a
6969
errors: Partial<ExtractStepResults<TSteps>>;
7070
};
7171

72-
// Extract TInput from the first step's validator function
72+
// Util: convert a union to an intersection
73+
type UnionToIntersection<U> = (U extends any ? (arg: U) => void : never) extends (arg: infer I) => void
74+
? I
75+
: never;
76+
77+
// Extract the *intersection* of all input types required by the steps. This guarantees that
78+
// the resulting processor knows about every property that any individual step relies on.
79+
// We purposely compute an intersection (not a union) so that all required fields are present.
7380
type ExtractInputType<TSteps extends readonly ValidationStepConfig<any, any, string>[]> =
74-
TSteps[number] extends ValidationStepConfig<infer TInput, any, string> ? TInput : never;
81+
UnionToIntersection<
82+
TSteps[number] extends ValidationStepConfig<infer TInput, any, string> ? TInput : never
83+
>;
7584

7685
/**
7786
* Creates a type-safe validation processor that executes a series of validation steps
@@ -162,17 +171,23 @@ type ExtractInputType<TSteps extends readonly ValidationStepConfig<any, any, str
162171
export function createValidationProcessor<
163172
const TSteps extends readonly ValidationStepConfig<any, any, string>[],
164173
>(definition: { steps: TSteps }) {
165-
type TInput = ExtractInputType<TSteps>;
174+
// Determine the base input type required by all steps (intersection).
175+
type BaseInput = ExtractInputType<TSteps>;
176+
177+
// Helper: widen input type for object literals while keeping regular objects assignable.
178+
type InputWithExtras = BaseInput extends object
179+
? BaseInput | (BaseInput & Record<string, unknown>)
180+
: BaseInput;
166181

167182
return function processValidation(
168-
input: TInput,
183+
input: InputWithExtras,
169184
config: ValidationPipelineConfig = {}
170185
): ValidationResult<TSteps> {
171186
const errors: Partial<ExtractStepResults<TSteps>> = {};
172187
let hasErrors = false;
173188

174189
for (const step of definition.steps) {
175-
const result = step.validator(input);
190+
const result = step.validator(input as BaseInput);
176191
const isError = step.isError(result);
177192

178193
if (isError) {

0 commit comments

Comments
 (0)