Skip to content

Commit c0177bf

Browse files
committed
feat(standard schema): support standard schema as patterns
1 parent 0e15315 commit c0177bf

7 files changed

Lines changed: 299 additions & 3 deletions

File tree

package-lock.json

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"prettier": "^2.8.8",
7979
"rimraf": "^5.0.1",
8080
"ts-jest": "^29.4.1",
81-
"typescript": "^5.9.2"
81+
"typescript": "^5.9.2",
82+
"zod": "^3.25.76"
8283
}
8384
}

src/internals/helpers.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
import * as symbols from './symbols';
88
import { SelectionType } from '../types/FindSelected';
99
import { Pattern, Matcher, MatcherType, AnyMatcher } from '../types/Pattern';
10+
import type { StandardSchemaV1 } from '../types/standard-schema/standard-schema-v1';
11+
12+
const STANDARD_SCHEMA_KEY = '~standard';
13+
14+
const isPromise = (value: unknown): value is Promise<unknown> =>
15+
typeof (value as any)?.then === 'function';
1016

1117
// @internal
1218
export const isObject = (value: unknown): value is Object =>
@@ -20,6 +26,22 @@ export const isMatcher = (
2026
return pattern && !!pattern[symbols.matcher];
2127
};
2228

29+
const isStandardSchema = (
30+
value: unknown
31+
): value is StandardSchemaV1<any, any> => {
32+
if (!isObject(value)) return false;
33+
const standard = (value as Record<string | symbol, unknown>)[
34+
STANDARD_SCHEMA_KEY
35+
];
36+
if (!isObject(standard)) return false;
37+
const { version, vendor, validate } = standard as StandardSchemaV1.Props;
38+
return (
39+
version === 1 &&
40+
typeof vendor === 'string' &&
41+
typeof validate === 'function'
42+
);
43+
};
44+
2345
// @internal
2446
const isOptionalPattern = (
2547
x: unknown
@@ -43,6 +65,16 @@ export const matchPattern = (
4365
return matched;
4466
}
4567

68+
if (isStandardSchema(pattern)) {
69+
const result = pattern[STANDARD_SCHEMA_KEY].validate(value);
70+
if (isPromise(result)) {
71+
throw new Error(
72+
'Async Standard Schema validation is not supported by ts-pattern.'
73+
);
74+
}
75+
return !Array.isArray(result.issues);
76+
}
77+
4678
if (isObject(pattern)) {
4779
if (!isObject(value)) return false;
4880

src/types/InvertPattern.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
RecordValue,
2626
} from './helpers';
2727
import type { Matcher, Pattern, Override, AnyMatcher } from './Pattern';
28+
import type { StandardSchemaV1 } from './standard-schema/standard-schema-v1';
2829

2930
type OptionalKeys<p> = ValueOf<{
3031
[k in keyof p]: 0 extends 1 & p[k] // inlining IsAny for perf
@@ -145,6 +146,8 @@ type InvertPatternInternal<p, input> = 0 extends 1 & p
145146
narrowedOrFn extends Fn ? Call<narrowedOrFn, input> : narrowedOrFn
146147
>;
147148
}[matcherType]
149+
: p extends StandardSchemaV1<any, any>
150+
? StandardSchemaV1.InferOutput<p>
148151
: p extends Primitives
149152
? p
150153
: p extends readonly any[]
@@ -370,6 +373,8 @@ type InvertPatternForExcludeInternal<p, i, empty = never> =
370373
? Call<narrowedOrFn, i>
371374
: excluded;
372375
}[matcherType]
376+
: p extends StandardSchemaV1<any, any>
377+
? StandardSchemaV1.InferInput<p>
373378
: p extends readonly any[]
374379
? Extract<i, readonly any[]> extends infer arrayInput
375380
? InvertArrayPatternForExclude<

src/types/Pattern.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MergeUnion, Primitives, WithDefault } from './helpers';
33
import { None, Some, SelectionType } from './FindSelected';
44
import { matcher } from '../patterns';
55
import { ExtractPreciseValue } from './ExtractPreciseValue';
6+
import type { StandardSchemaV1 } from './standard-schema/standard-schema-v1';
67

78
export type MatcherType =
89
| 'not'
@@ -80,6 +81,8 @@ export type AnyMatcher = Matcher<any, any, any, any, any>;
8081

8182
type UnknownMatcher = PatternMatcher<unknown>;
8283

84+
type StandardSchemaPattern = StandardSchemaV1;
85+
8386
export type CustomP<input, pattern, narrowedOrFn> = Matcher<
8487
input,
8588
pattern,
@@ -141,7 +144,8 @@ export type UnknownValuePattern =
141144
| readonly [...unknown[], unknown]
142145
| UnknownProperties
143146
| Primitives
144-
| UnknownMatcher;
147+
| UnknownMatcher
148+
| StandardSchemaPattern;
145149

146150
/**
147151
* `Pattern<a>` is the generic type for patterns matching a value of type `a`. A pattern can be any (nested) javascript value.
@@ -168,6 +172,7 @@ type KnownPatternInternal<
168172
> =
169173
| primitives
170174
| PatternMatcher<a>
175+
| StandardSchemaPattern
171176
| ([objs] extends [never] ? never : ObjectPattern<Readonly<MergeUnion<objs>>>)
172177
| ([arrays] extends [never] ? never : ArrayPattern<arrays>);
173178

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/** The Standard Schema interface. */
2+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
3+
/** The Standard Schema properties. */
4+
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
5+
}
6+
7+
export declare namespace StandardSchemaV1 {
8+
/** The Standard Schema properties interface. */
9+
export interface Props<Input = unknown, Output = Input> {
10+
/** The version number of the standard. */
11+
readonly version: 1;
12+
/** The vendor name of the schema library. */
13+
readonly vendor: string;
14+
/** Validates unknown input values. */
15+
readonly validate: (
16+
value: unknown
17+
) => Result<Output> | Promise<Result<Output>>;
18+
/** Inferred types associated with the schema. */
19+
readonly types?: Types<Input, Output> | undefined;
20+
}
21+
22+
/** The result interface of the validate function. */
23+
export type Result<Output> = SuccessResult<Output> | FailureResult;
24+
25+
/** The result interface if validation succeeds. */
26+
export interface SuccessResult<Output> {
27+
/** The typed output value. */
28+
readonly value: Output;
29+
/** The non-existent issues. */
30+
readonly issues?: undefined;
31+
}
32+
33+
/** The result interface if validation fails. */
34+
export interface FailureResult {
35+
/** The issues of failed validation. */
36+
readonly issues: ReadonlyArray<Issue>;
37+
}
38+
39+
/** The issue interface of the failure output. */
40+
export interface Issue {
41+
/** The error message of the issue. */
42+
readonly message: string;
43+
/** The path of the issue, if any. */
44+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
45+
}
46+
47+
/** The path segment interface of the issue. */
48+
export interface PathSegment {
49+
/** The key representing a path segment. */
50+
readonly key: PropertyKey;
51+
}
52+
53+
/** The Standard Schema types interface. */
54+
export interface Types<Input = unknown, Output = Input> {
55+
/** The input type of the schema. */
56+
readonly input: Input;
57+
/** The output type of the schema. */
58+
readonly output: Output;
59+
}
60+
61+
/** Infers the input type of a Standard Schema. */
62+
export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
63+
Schema['~standard']['types']
64+
>['input'];
65+
66+
/** Infers the output type of a Standard Schema. */
67+
export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
68+
Schema['~standard']['types']
69+
>['output'];
70+
71+
// biome-ignore lint/complexity/noUselessEmptyExport: needed for granular visibility control of TS namespace
72+
export {};
73+
}

0 commit comments

Comments
 (0)