Skip to content
69 changes: 69 additions & 0 deletions library/src/actions/guard/guard.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expectTypeOf, test } from 'vitest';
import { pipe } from '../../methods/index.ts';
import { number, string } from '../../schemas/index.ts';
import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts';
import type { GuardAction, GuardIssue } from './guard.ts';
import { guard } from './guard.ts';

describe('guard', () => {
type PixelString = `${number}px`;
const isPixelString = (input: string): input is PixelString =>
/^\d+px$/u.test(input);

describe('should return action object', () => {
test('with no message', () => {
expectTypeOf(guard(isPixelString)).toEqualTypeOf<
GuardAction<string, PixelString, undefined>
>();
});
test('with string message', () => {
expectTypeOf(
guard<string, PixelString, 'message'>(isPixelString, 'message')
).toEqualTypeOf<GuardAction<string, PixelString, 'message'>>();
});

test('with function message', () => {
expectTypeOf(
guard<string, PixelString, () => string>(isPixelString, () => 'message')
).toEqualTypeOf<GuardAction<string, PixelString, () => string>>();
});
});

describe('should infer correct types', () => {
test('of input', () => {
expectTypeOf<
InferInput<GuardAction<string, PixelString, undefined>>
>().toEqualTypeOf<string>();
});

test('of output', () => {
expectTypeOf<
InferOutput<GuardAction<string, PixelString, undefined>>
>().toEqualTypeOf<PixelString>();
});

test('of issue', () => {
expectTypeOf<
InferIssue<GuardAction<string, PixelString, undefined>>
>().toEqualTypeOf<GuardIssue<string, PixelString>>();
});
});

test('should infer correct type in pipe', () => {
pipe(
string(),
guard((input) => {
expectTypeOf(input).toEqualTypeOf<string>();
return isPixelString(input);
})
);
});

test("should error if pipe input doesn't match", () => {
pipe(
number(),
// @ts-expect-error
guard(isPixelString)
);
});
});
89 changes: 89 additions & 0 deletions library/src/actions/guard/guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, test } from 'vitest';
import type { FailureDataset } from '../../types/dataset.ts';
import type { GuardAction, GuardIssue } from './guard.ts';
import { guard } from './guard.ts';

describe('guard', () => {
type PixelString = `${number}px`;
const isPixelString = (input: string): input is PixelString =>
/^\d+px$/u.test(input);

const baseAction: Omit<
GuardAction<string, PixelString, undefined>,
'message'
> = {
kind: 'transformation',
type: 'guard',
reference: guard,
requirement: isPixelString,
async: false,
'~run': expect.any(Function),
};

describe('should return action object', () => {
test('with undefined message', () => {
const action: GuardAction<string, PixelString, undefined> = {
...baseAction,
message: undefined,
};
expect(guard(isPixelString)).toStrictEqual(action);
expect(guard(isPixelString, undefined)).toStrictEqual(action);
});

test('with string message', () => {
const action: GuardAction<string, PixelString, 'message'> = {
...baseAction,
message: 'message',
};
expect(guard(isPixelString, 'message')).toStrictEqual(action);
});

test('with function message', () => {
const message = () => 'message';
const action: GuardAction<string, PixelString, typeof message> = {
...baseAction,
message,
};
expect(guard(isPixelString, message)).toStrictEqual(action);
});
});

test('should return dataset without issues', () => {
const action = guard(isPixelString);
const outputDataset = { typed: true, value: '123px' };
expect(action['~run']({ typed: true, value: '123px' }, {})).toStrictEqual(
outputDataset
);
});

test('should return dataset with issues', () => {
const action = guard(isPixelString, 'message');
const baseIssue: Omit<
GuardIssue<string, PixelString>,
'input' | 'received'
> = {
kind: 'transformation',
type: 'guard',
expected: null,
message: 'message',
requirement: isPixelString,
path: undefined,
issues: undefined,
lang: undefined,
abortEarly: undefined,
abortPipeEarly: undefined,
};

expect(action['~run']({ typed: true, value: '123' }, {})).toStrictEqual({
typed: false,
value: '123',
issues: [
{
...baseIssue,
input: '123',
received: '"123"',
},
],
} satisfies FailureDataset<GuardIssue<string, PixelString>>);
});
});
109 changes: 109 additions & 0 deletions library/src/actions/guard/guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type {
BaseIssue,
BaseTransformation,
ErrorMessage,
} from '../../types/index.ts';
import { _addIssue } from '../../utils/index.ts';

export type Guard<TInput, TOutput extends TInput> = (
input: TInput
) => input is TOutput;

/**
* Guard issue interface.
*/
export interface GuardIssue<TInput, TOutput extends TInput>
extends BaseIssue<TInput> {
/**
* The issue kind.
*/
readonly kind: 'transformation';
/**
* The validation type.
*/
readonly type: 'guard';
/**
* The validation requirement.
*/
readonly requirement: Guard<TInput, TOutput>;
}

/**
* Guard action interface.
*/
export interface GuardAction<
TInput,
TOutput extends TInput,
TMessage extends ErrorMessage<GuardIssue<TInput, TOutput>> | undefined,
> extends BaseTransformation<TInput, TOutput, GuardIssue<TInput, TOutput>> {
/**
* The action type.
*/
readonly type: 'guard';
/**
* The action reference.
*/
readonly reference: typeof guard;
/**
* The guard function.
*/
readonly requirement: Guard<TInput, TOutput>;
/**
* The error message.
*/
readonly message: TMessage;
}
/**
* Creates a guard validation action.
*
* @param requirement The guard function.
*
* @returns A guard action.
*/
export function guard<TInput, TOutput extends TInput>(
requirement: Guard<TInput, TOutput>
): GuardAction<TInput, TOutput, undefined>;

/**
* Creates a guard validation action.
*
* @param requirement The guard function.
* @param message The error message.
*
* @returns A guard action.
*/
export function guard<
TInput,
TOutput extends TInput,
const TMessage extends ErrorMessage<GuardIssue<TInput, TOutput>> | undefined,
>(
requirement: Guard<TInput, TOutput>,
message: TMessage
): GuardAction<TInput, TOutput, TMessage>;

// @__NO_SIDE_EFFECTS__
export function guard(
requirement: Guard<unknown, unknown>,
message?: ErrorMessage<GuardIssue<unknown, unknown>>
): GuardAction<
unknown,
unknown,
ErrorMessage<GuardIssue<unknown, unknown>> | undefined
> {
return {
kind: 'transformation',
type: 'guard',
reference: guard,
async: false,
requirement,
message,
'~run'(dataset, config) {
if (dataset.typed && !this.requirement(dataset.value)) {
_addIssue(this, 'input', dataset, config);
// @ts-expect-error
dataset.typed = false;
}
return dataset;
},
};
}
1 change: 1 addition & 0 deletions library/src/actions/guard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './guard.ts';
1 change: 1 addition & 0 deletions library/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from './finite/index.ts';
export * from './flavor/index.ts';
export * from './graphemes/index.ts';
export * from './gtValue/index.ts';
export * from './guard/index.ts';
export * from './hash/index.ts';
export * from './hexadecimal/index.ts';
export * from './hexColor/index.ts';
Expand Down
91 changes: 91 additions & 0 deletions website/src/routes/api/(actions)/guard/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
title: guard
description: Creates a guard transformation action.
source: /actions/guard/guard.ts
contributors:
- EskiMojo14
---

import { ApiList, Property } from '~/components';
import { properties } from './properties';

# guard

Creates a guard transformation action.

```ts
const Action = v.guard<TInput, TOutput, TMessage>(requirement, message);
```

## Generics

- `TInput` <Property {...properties.TInput} />
- `TOutput` <Property {...properties.TOutput} />
- `TMessage` <Property {...properties.TMessage} />

## Parameters

- `requirement` <Property {...properties.requirement} />
- `message` <Property {...properties.message} />

### Explanation

With `guard` you can freely validate the input and return `true` if it is valid or `false` otherwise. If the input does not match your `requirement`, you can use `message` to customize the error message.

This is especially useful if you have an existing type predicate (for example, from an external library).

> `guard` is useful for narrowing known types. For validating completely unknown values, consider [`custom`](../custom/) instead.

## Returns

- `Action` <Property {...properties.Action} />

## Examples

The following examples show how `guard` can be used.

### Pixel string schema

Schema to validate a pixel string.

```ts
const PixelStringSchema = v.pipe(
v.string(),
v.guard((input): input is `${number}px` => /^\d+px$/.test(input))
);
```

### Axios Error schema

Schema to validate an object containing an Axios error.

```ts
import { isAxiosError } from 'axios';

const AxiosErrorSchema = v.object({
error: v.pipe(
v.instance(Error),
v.guard(isAxiosError, 'The error is not an Axios error.')
),
});
```

## Related

The following APIs can be combined with `guard`.

### Schemas

<ApiList items={['any', 'custom', 'instance', 'object', 'unknown']} />

### Methods

<ApiList items={['pipe']} />

### Utils

<ApiList items={['isOfKind', 'isOfType']} />

### Actions

<ApiList items={['check', 'rawCheck']} />
Loading