Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved coerce and transform #197

Closed
sillvva opened this issue Oct 4, 2023 · 5 comments
Closed

Improved coerce and transform #197

sillvva opened this issue Oct 4, 2023 · 5 comments
Assignees
Labels
enhancement New feature or request priority This has priority

Comments

@sillvva
Copy link
Contributor

sillvva commented Oct 4, 2023

As described in this discussion:

What I have found in docs

const NumberSchema = coerce(number(), Number);
const numberOutput = parse(NumberSchema, '1234'); // 1234

While string can be passed here, its Input<typeof NumberSchema> returns number.

transform(string(), Number);

Sets correct Input (string) and Output (number) types, but validation happens against string, before the transformation takes place.

I think the above functions can be improved. With the following, coerce could accept an input type.

export function coerce<TInput, TOutput extends BaseSchema>(
	schema: TOutput,
	action: (value: TInput) => Input<TOutput>
): BaseSchema<TInput, Output<TOutput>> {
	return {
		...schema,
		_parse(input: TInput, info?: ParseInfo) {
			return schema._parse(action(input), info);
		}
	};
}

const isNumber = coerce(
	number([custom((input) => !isNaN(input), "Not a number")]),
	// ^ Output type
	(input: string) => Number(input)
	// ^ Input type
);

export type IsNumberIn = Input<typeof isNumber>;
//           ^? type IsNumberIn = string
export type IsNumberOut = Output<typeof isNumber>;
//           ^? type IsNumberOut = number

console.log(isNumber._parse("1")); // OK
console.log(isNumber._parse(1));   // OK, BaseSchema makes the parse input "unknown"
console.log(isNumber._parse("a")); // Error

And the following is a custom function called ioTransform which accepts both an input and output schema

export function ioTransform<TInput extends BaseSchema, TOutput extends BaseSchema>(
	inputSchema: TInput,
	outputSchema: TOutput,
	action: (value: Input<TInput>) => Input<TOutput>
): BaseSchema<Input<TInput>, Output<TOutput>> {
	return {
		...outputSchema,
		_parse(input: Input<TInput>, info?: ParseInfo) {
			const validation = inputSchema._parse(input, info);
			if (validation.issues) return validation;
			return outputSchema._parse(action(input), info);
		}
	};
}

const isNumber = ioTransform(
	string(), 
	number([custom((input) => !isNaN(input), "Not a number")]), 
	(input) => Number(input)
);

export type IsNumberIn = Input<typeof isNumber>;
//           ^? type IsNumberIn = string
export type IsNumberOut = Output<typeof isNumber>;
//           ^? type IsNumberOut = number

console.log(isNumber._parse("1")); // OK
console.log(isNumber._parse(1));   // Error
console.log(isNumber._parse("a")); // Error

const toNumber = ioTransform(
	unknown(), 
	number([custom((input) => !isNaN(input), "Not a number")]), 
	(input) => Number(input)
);

export type ToNumberIn = Input<typeof toNumber>;
//           ^? type ToNumberIn = unknown
export type ToNumberOut = Output<typeof toNumber>;
//           ^? type ToNumberOut = number

console.log(toNumber._parse("1")); // OK
console.log(toNumber._parse(1));   // OK
console.log(toNumber._parse("a")); // Error
@fabian-hiller
Copy link
Owner

We can't do (input: string) => … like in the following code example. It is basically wrong typing. The input what not validated before, so we can't know if it is a string.

const IntegerSchema = coerce(
  number([integer("Not an integer")]),
  // ^ Output type
  (input: string) => Number(input)
  // ^ Input type
);

But I see the point, that sometimes you want to validate the input, transform it and also validate the output. We should find an official solution for this. At the moment I am not sure if we should put it in coerce, transform oder add a new method for this.

Here is an example with transform where the arguments follow a logical order:

const IntegerSchema = transform(
  string(),
  (input) => Number(input),
  number([integer("Not an integer")])
);

@fabian-hiller fabian-hiller self-assigned this Oct 5, 2023
@fabian-hiller fabian-hiller added enhancement New feature or request priority This has priority labels Oct 5, 2023
@sillvva
Copy link
Contributor Author

sillvva commented Oct 5, 2023

I like the idea of transform following a logical order.

Currently, transform accepts an input schema and an action. It already returns the correct types, but you could add an output schema as an optional third argument to validate the output.

export function transform<TInputSchema extends BaseSchema, TOutput, TOutputSchema extends BaseSchema<unknown, TOutput>>(
	inputSchema: TInputSchema,
	action: (value: Input<TInputSchema>) => TOutput,
	outputSchema?: TOutputSchema
): BaseSchema<Input<TInputSchema>, TOutput> {
	return {
		...inputSchema,
		_parse(input, info) {
			const result = inputSchema._parse(input, info);
			if (result.issues) return result;
			if (!outputSchema) return getOutput(action(result.output));

			const output = outputSchema._parse(action(input), info);
			return output.issues ? output : getOutput(output.output);
		}
	};
}

const toNumber1 = transform(string(), Number);
type ToNumber1In = Input<typeof toNumber1>;
//    ^? string
type ToNumber1Out = Output<typeof toNumber1>;
//    ^? number
console.log(toNumber1._parse("1.2"));	// 1.2 = OK
console.log(toNumber1._parse(1)); 		// Error
console.log(toNumber1._parse("a")); 	// NaN = OK

const toNumber2 = transform(string(), Number, number([custom((input) => !isNaN(input), "Not a number")]));
type ToNumber2In = Input<typeof toNumber2>;
//    ^? string
type ToNumber2Out = Output<typeof toNumber2>;
//    ^? number
console.log(toNumber2._parse("1.2"));	// 1.2 = OK
console.log(toNumber2._parse(1)); 		// Error
console.log(toNumber2._parse("a")); 	// Error

@fabian-hiller
Copy link
Owner

Thank you for your feedback. I think that I will bring this feature with the next version.

@fabian-hiller
Copy link
Owner

I have now examined the implementation. I noticed that the data type of the output does not need to be validated, since we already know it. So instead of passing an entire schema as a third argument, it should be sufficient to pass a pipeline.

@fabian-hiller
Copy link
Owner

Improved in v0.19.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request priority This has priority
Projects
None yet
Development

No branches or pull requests

2 participants