Skip to content

Docs: Imperative invalidation caveat #14745

@sillvva

Description

@sillvva

Describe the problem

Unlike redirect() and error(), the new invalid() function in remote forms does not block the TypeScript control flow analysis, even though it throws internally and has a never return type.

import { form } from '$app/server';
import * as v from 'valibot';

export const testForm = form(
	v.object({
		test: v.union([v.string(), v.number()])
	}),
	(input, invalid) => {
		if (typeof input.test == "number") invalid(invalid.test("Should not be a number"));
		console.log(input.test);
		//                ^? string | number
		if (typeof input.test == "number") throw invalid(invalid.test("Should not be a number"));
		console.log(input.test);
		//                ^? string
		return input;
	}
);

Describe the proposed solution

I feel like this should be mentioned in the docs. It already mentions that it throws a validation error, but without an explicit throw invalid() it won't narrow types.

- Call `invalid(issue1, issue2, ...issueN)` to throw a validation error
- Like `error()` and `redirect()`, it throws an error internally. However, unlike 
  `error()` and `redirect()`, it must be explicitly thrown to block the TypeScript 
  control flow analysis. Example: `throw invalid(issue1, issue2, ...issueN)`

Alternatives considered

I'm not sure if this is possible to fix.

Currently the code is this:

export type Invalid<Input = any> = ((...issues: Array<string | StandardSchemaV1.Issue>) => never) & InvalidField<Input>;

I tried this and got the same result:

export type Invalid<Input = any> = {
	(...issues: Array<string | StandardSchemaV1.Issue>): never;
} & InvalidField<Input>;

Unlike redirect() and error() which are functions exported directly, invalid() is returned as a callback parameter and I think TS is not treating it the same, even though they have similar declarations:

/**
 * ...
 * @return {never}
 */
export function redirect(status, location) { ... }

/**
 * ...
 * @returns {never}
 */
function invalid(...issues) { ... }

Importance

nice to have

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions