Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions library/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export default tseslint.config(
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/consistent-indexed-object-style': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-namespace': 'off',

// Imports
'no-duplicate-imports': 'off',
Expand Down
34 changes: 18 additions & 16 deletions library/src/actions/args/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,28 @@ import type {
} from '../../types/index.ts';
import { ValiError } from '../../utils/index.ts';

/**
* Schema type.
*/
type Schema =
| LooseTupleSchema<TupleItems, ErrorMessage<LooseTupleIssue> | undefined>
| StrictTupleSchema<TupleItems, ErrorMessage<StrictTupleIssue> | undefined>
| TupleSchema<TupleItems, ErrorMessage<TupleIssue> | undefined>
| TupleWithRestSchema<
TupleItems,
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
ErrorMessage<TupleWithRestIssue> | undefined
>;
export namespace args {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool PR!

If you change these to ambient namespaces it will enforce that no values are added to these namespaces. It will also help with the erasableSyntaxOnly flag

Suggested change
export namespace args {
export declare namespace args {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, I'm still allowed to do export const in an ambient namespace

erasableSyntaxOnly is a good shout, worth checking if anything valibot is doing violates that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, though that was added in TS 5.8 and this repo is currently on 5.7.3

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend use ambient namespaces by default. And if Valibot uses 5.7.3, in userland if an enum or value-level namespace is used, it can't be erased for users that are on later versions of TS

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just checked - no source code uses enums, but there are some used in tests for schemas that support native enums

Copy link
Contributor

@ahrjarrett ahrjarrett Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify -- if you write export const x = 1 in an ambient namespace, the namespace and x will be erased at runtime.

If you write export const x = 1 in a regular namespace, then namespace won't be erased, and x will be shipped with the JS bundle

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but the const is still in the type declarations so as far as typescript is concerned it exists, which seems worse imo

Copy link
Contributor

@ahrjarrett ahrjarrett Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's a concern, then I recommend this:

export declare namespace args {
  export type { Schema }
}

type Schema = // ...

That way there's no pattern of adding anything to the namespace itself. It also allows users to access Schema in both places

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think I'd rather leave it as is, and maybe we get a follow up PR for updating TS and enabling erasableSyntaxOnly if that's desired

Copy link
Contributor

@ahrjarrett ahrjarrett Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff would be much smaller that way too 🤷 it can only help your chances of getting it merged

/**
* Schema type.
*/
export type Schema =
| LooseTupleSchema<TupleItems, ErrorMessage<LooseTupleIssue> | undefined>
| StrictTupleSchema<TupleItems, ErrorMessage<StrictTupleIssue> | undefined>
| TupleSchema<TupleItems, ErrorMessage<TupleIssue> | undefined>
| TupleWithRestSchema<
TupleItems,
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
ErrorMessage<TupleWithRestIssue> | undefined
>;
}

/**
* Args action type.
*/
export interface ArgsAction<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TInput extends (...args: any[]) => unknown,
TSchema extends Schema,
TSchema extends args.Schema,
> extends BaseTransformation<
TInput,
(...args: InferInput<TSchema>) => ReturnType<TInput>,
Expand Down Expand Up @@ -67,13 +69,13 @@ export interface ArgsAction<
export function args<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TInput extends (...args: any[]) => unknown,
TSchema extends Schema,
TSchema extends args.Schema,
>(schema: TSchema): ArgsAction<TInput, TSchema>;

// @__NO_SIDE_EFFECTS__
export function args(
schema: Schema
): ArgsAction<(...args: unknown[]) => unknown, Schema> {
schema: args.Schema
): ArgsAction<(...args: unknown[]) => unknown, args.Schema> {
return {
kind: 'transformation',
type: 'args',
Expand Down
64 changes: 33 additions & 31 deletions library/src/actions/args/argsAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,41 +25,43 @@ import type {
} from '../../types/index.ts';
import { ValiError } from '../../utils/index.ts';

/**
* Schema type.
*/
type Schema =
| LooseTupleSchema<TupleItems, ErrorMessage<LooseTupleIssue> | undefined>
| LooseTupleSchemaAsync<
TupleItemsAsync,
ErrorMessage<LooseTupleIssue> | undefined
>
| StrictTupleSchema<TupleItems, ErrorMessage<StrictTupleIssue> | undefined>
| StrictTupleSchemaAsync<
TupleItemsAsync,
ErrorMessage<StrictTupleIssue> | undefined
>
| TupleSchema<TupleItems, ErrorMessage<TupleIssue> | undefined>
| TupleSchemaAsync<TupleItemsAsync, ErrorMessage<TupleIssue> | undefined>
| TupleWithRestSchema<
TupleItems,
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
ErrorMessage<TupleWithRestIssue> | undefined
>
| TupleWithRestSchemaAsync<
TupleItemsAsync,
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>,
ErrorMessage<TupleWithRestIssue> | undefined
>;
export namespace argsAsync {
/**
* Schema type.
*/
export type Schema =
| LooseTupleSchema<TupleItems, ErrorMessage<LooseTupleIssue> | undefined>
| LooseTupleSchemaAsync<
TupleItemsAsync,
ErrorMessage<LooseTupleIssue> | undefined
>
| StrictTupleSchema<TupleItems, ErrorMessage<StrictTupleIssue> | undefined>
| StrictTupleSchemaAsync<
TupleItemsAsync,
ErrorMessage<StrictTupleIssue> | undefined
>
| TupleSchema<TupleItems, ErrorMessage<TupleIssue> | undefined>
| TupleSchemaAsync<TupleItemsAsync, ErrorMessage<TupleIssue> | undefined>
| TupleWithRestSchema<
TupleItems,
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
ErrorMessage<TupleWithRestIssue> | undefined
>
| TupleWithRestSchemaAsync<
TupleItemsAsync,
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>,
ErrorMessage<TupleWithRestIssue> | undefined
>;
}

/**
* Args action async type.
*/
export interface ArgsActionAsync<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TInput extends (...args: any[]) => unknown,
TSchema extends Schema,
TSchema extends argsAsync.Schema,
> extends BaseTransformation<
TInput,
(...args: InferInput<TSchema>) => Promise<Awaited<ReturnType<TInput>>>,
Expand Down Expand Up @@ -89,13 +91,13 @@ export interface ArgsActionAsync<
export function argsAsync<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TInput extends (...args: any[]) => unknown,
TSchema extends Schema,
TSchema extends argsAsync.Schema,
>(schema: TSchema): ArgsActionAsync<TInput, TSchema>;

// @__NO_SIDE_EFFECTS__
export function argsAsync(
schema: Schema
): ArgsActionAsync<(...args: unknown[]) => unknown, Schema> {
schema: argsAsync.Schema
): ArgsActionAsync<(...args: unknown[]) => unknown, argsAsync.Schema> {
return {
kind: 'transformation',
type: 'args',
Expand Down
62 changes: 33 additions & 29 deletions library/src/methods/getDescription/getDescription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,36 @@ import type {
import { _getLastMetadata } from '../../utils/index.ts';
import type { SchemaWithPipe, SchemaWithPipeAsync } from '../index.ts';

/**
* Schema type.
*/
type Schema =
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
| SchemaWithPipe<
readonly [
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| DescriptionAction<unknown, string>
)[],
]
>
| SchemaWithPipeAsync<
readonly [
(
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
),
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| PipeItemAsync<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| DescriptionAction<unknown, string>
)[],
]
>;
export namespace getDescription {
/**
* Schema type.
*/
export type Schema =
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
| SchemaWithPipe<
readonly [
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| DescriptionAction<unknown, string>
)[],
]
>
| SchemaWithPipeAsync<
readonly [
(
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
),
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| PipeItemAsync<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| DescriptionAction<unknown, string>
)[],
]
>;
}

/**
* Returns the description of the schema.
Expand All @@ -52,6 +54,8 @@ type Schema =
*/
// TODO: Investigate if return type can be strongly typed
// @__NO_SIDE_EFFECTS__
export function getDescription(schema: Schema): string | undefined {
export function getDescription(
schema: getDescription.Schema
): string | undefined {
return _getLastMetadata(schema, 'description');
}
64 changes: 33 additions & 31 deletions library/src/methods/getMetadata/getMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,36 @@ import type {
} from '../../types/index.ts';
import type { SchemaWithPipe, SchemaWithPipeAsync } from '../pipe/index.ts';

/**
* Schema type.
*/
type Schema =
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
| SchemaWithPipe<
readonly [
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| MetadataAction<unknown, Record<string, unknown>>
)[],
]
>
| SchemaWithPipeAsync<
readonly [
(
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
),
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| PipeItemAsync<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| MetadataAction<unknown, Record<string, unknown>>
)[],
]
>;
export namespace getMetadata {
/**
* Schema type.
*/
export type Schema =
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
| SchemaWithPipe<
readonly [
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| MetadataAction<unknown, Record<string, unknown>>
)[],
]
>
| SchemaWithPipeAsync<
readonly [
(
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
),
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| PipeItemAsync<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| MetadataAction<unknown, Record<string, unknown>>
)[],
]
>;
}

/**
* Basic pipe item type.
Expand Down Expand Up @@ -74,7 +76,7 @@ type RecursiveMerge<
*
* @beta
*/
export type InferMetadata<TSchema extends Schema> =
export type InferMetadata<TSchema extends getMetadata.Schema> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
BaseSchema<any, any, any> extends TSchema
? Record<string, unknown>
Expand All @@ -101,11 +103,11 @@ export type InferMetadata<TSchema extends Schema> =
* @beta
*/
// @__NO_SIDE_EFFECTS__
export function getMetadata<const TSchema extends Schema>(
export function getMetadata<const TSchema extends getMetadata.Schema>(
schema: TSchema
): InferMetadata<TSchema> {
const result = {};
function depthFirstMerge(schema: Schema): void {
function depthFirstMerge(schema: getMetadata.Schema): void {
if ('pipe' in schema) {
for (const item of schema.pipe) {
if (item.kind === 'schema' && 'pipe' in item) {
Expand Down
61 changes: 31 additions & 30 deletions library/src/methods/getTitle/getTitle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,36 @@ import type {
import { _getLastMetadata } from '../../utils/index.ts';
import type { SchemaWithPipe, SchemaWithPipeAsync } from '../index.ts';

/**
* Schema type.
*/
type Schema =
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
| SchemaWithPipe<
readonly [
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| TitleAction<unknown, string>
)[],
]
>
| SchemaWithPipeAsync<
readonly [
(
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
),
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| PipeItemAsync<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| TitleAction<unknown, string>
)[],
]
>;

export namespace getTitle {
/**
* Schema type.
*/
export type Schema =
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
| SchemaWithPipe<
readonly [
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| TitleAction<unknown, string>
)[],
]
>
| SchemaWithPipeAsync<
readonly [
(
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>
),
...(
| PipeItem<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| PipeItemAsync<any, unknown, BaseIssue<unknown>> // eslint-disable-line @typescript-eslint/no-explicit-any
| TitleAction<unknown, string>
)[],
]
>;
}
/**
* Returns the title of the schema.
*
Expand All @@ -52,6 +53,6 @@ type Schema =
*/
// TODO: Investigate if return type can be strongly typed
// @__NO_SIDE_EFFECTS__
export function getTitle(schema: Schema): string | undefined {
export function getTitle(schema: getTitle.Schema): string | undefined {
return _getLastMetadata(schema, 'title');
}
Loading