Skip to content

Commit b32779d

Browse files
committed
Enhance S.union for TS API
1 parent 64b5b1d commit b32779d

8 files changed

Lines changed: 142 additions & 30 deletions

File tree

IDEAS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ let trimContract: S.contract<string => string> = S.contract(s => {
4444
- Change asyncParser from () => () => promise to () => promise
4545
- Add schema input to the error ??? What about build errors?
4646
- Improve S.schema performance and expose it to the JS/TS API instead of S.object shorthand
47+
- Get rid of S.literal in Js/ts API
48+
// FIXME: Return back s.flatten support for schemas created with S.schema
4749

4850
### Done
4951

@@ -55,6 +57,7 @@ let trimContract: S.contract<string => string> = S.contract(s => {
5557
- S.to/S.variant don't allow to distructure tuples anymore
5658
- Shorthand version for S.schema in Js/Ts API TODO: Update docs to use it by default
5759
- Fix tuple parsing nested in an object schema
60+
- Add shorthand syntax for S.union in Js/Ts API
5861

5962
## v10
6063

packages/tests/src/benchmark/comparison.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ const valibotSchema = V.object({
4949
S.setGlobalConfig({
5050
disableNanNumberCheck: true,
5151
});
52-
const schema = S.object({
52+
const schema = S.schema({
5353
number: S.number,
5454
negNumber: S.number,
5555
maxNumber: S.number,
5656
string: S.string,
5757
longString: S.string,
5858
boolean: S.boolean,
59-
deeplyNested: S.object({
59+
deeplyNested: S.schema({
6060
foo: S.string,
6161
num: S.number,
6262
bool: S.boolean,
@@ -134,14 +134,14 @@ new B.Suite()
134134
return V.parse(valibotSchema, data);
135135
})
136136
.add("rescript-schema (create)", () => {
137-
return S.object({
137+
return S.schema({
138138
number: S.number,
139139
negNumber: S.number,
140140
maxNumber: S.number,
141141
string: S.string,
142142
longString: S.string,
143143
boolean: S.boolean,
144-
deeplyNested: S.object({
144+
deeplyNested: S.schema({
145145
foo: S.string,
146146
num: S.number,
147147
bool: S.boolean,
@@ -152,14 +152,14 @@ new B.Suite()
152152
return S.parseWith(data, schema);
153153
})
154154
.add("rescript-schema (create + parse)", () => {
155-
const schema = S.object({
155+
const schema = S.schema({
156156
number: S.number,
157157
negNumber: S.number,
158158
maxNumber: S.number,
159159
string: S.string,
160160
longString: S.string,
161161
boolean: S.boolean,
162-
deeplyNested: S.object({
162+
deeplyNested: S.schema({
163163
foo: S.string,
164164
num: S.number,
165165
bool: S.boolean,

packages/tests/src/core/S_test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,6 +1331,57 @@ test("Successfully parses union", (t) => {
13311331
>(true);
13321332
});
13331333

1334+
test("Shape union", (t) => {
1335+
const shapeSchema = S.union([
1336+
{
1337+
kind: "circle" as const,
1338+
radius: S.number,
1339+
},
1340+
{
1341+
kind: "square" as const,
1342+
x: S.number,
1343+
},
1344+
{
1345+
kind: "triangle" as const,
1346+
x: S.number,
1347+
y: S.number,
1348+
},
1349+
]);
1350+
const value = S.parseWith(
1351+
{
1352+
kind: "circle",
1353+
radius: 123,
1354+
},
1355+
shapeSchema
1356+
);
1357+
1358+
t.deepEqual(value, {
1359+
kind: "circle",
1360+
radius: 123,
1361+
});
1362+
1363+
expectType<
1364+
TypeEqual<
1365+
typeof shapeSchema,
1366+
S.Schema<
1367+
| {
1368+
kind: "circle";
1369+
radius: number;
1370+
}
1371+
| {
1372+
kind: "square";
1373+
x: number;
1374+
}
1375+
| {
1376+
kind: "triangle";
1377+
x: number;
1378+
y: number;
1379+
}
1380+
>
1381+
>
1382+
>(true);
1383+
});
1384+
13341385
test("Successfully parses union with transformed items", (t) => {
13351386
const schema = S.union([
13361387
S.transform(S.string, (string) => Number(string)),

src/S.d.ts

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,27 @@ export type Output<T> = T extends Schema<infer Output, unknown>
1414
export type Input<T> = T extends Schema<unknown, infer Input> ? Input : never;
1515

1616
type UnknownSchema = Schema<unknown>;
17+
18+
type UnknownToOuput<T> = T extends Schema<unknown>
19+
? Output<T>
20+
: T extends {
21+
[k in keyof T]: unknown;
22+
}
23+
? {
24+
[k in keyof T]: UnknownToOuput<T[k]>;
25+
}
26+
: T;
27+
28+
type UnknownToInput<T> = T extends Schema<unknown>
29+
? Input<T>
30+
: T extends {
31+
[k in keyof T]: unknown;
32+
}
33+
? {
34+
[k in keyof T]: UnknownToInput<T[k]>;
35+
}
36+
: T;
37+
1738
type SchemaTupleOutput<
1839
Tuple extends UnknownSchema[],
1940
Length extends number = Tuple["length"]
@@ -47,6 +68,47 @@ type _TupleInput<
4768
? Accumulated
4869
: _TupleInput<Tuple, Length, [...Accumulated, Input<Tuple[Index]>]>;
4970

71+
type SchemaUnionTupleOutput<
72+
Tuple extends unknown[],
73+
Length extends number = Tuple["length"]
74+
> = Length extends Length
75+
? number extends Length
76+
? Tuple
77+
: _UnionTupleOutput<Tuple, Length, []>
78+
: never;
79+
type _UnionTupleOutput<
80+
Tuple extends unknown[],
81+
Length extends number,
82+
Accumulated extends unknown[],
83+
Index extends number = Accumulated["length"]
84+
> = Index extends Length
85+
? Accumulated
86+
: _UnionTupleOutput<
87+
Tuple,
88+
Length,
89+
[...Accumulated, UnknownToOuput<Tuple[Index]>]
90+
>;
91+
type SchemaUnionTupleInput<
92+
Tuple extends unknown[],
93+
Length extends number = Tuple["length"]
94+
> = Length extends Length
95+
? number extends Length
96+
? Tuple
97+
: _UnionTupleInput<Tuple, Length, []>
98+
: never;
99+
type _UnionTupleInput<
100+
Tuple extends unknown[],
101+
Length extends number,
102+
Accumulated extends unknown[],
103+
Index extends number = Accumulated["length"]
104+
> = Index extends Length
105+
? Accumulated
106+
: _UnionTupleInput<
107+
Tuple,
108+
Length,
109+
[...Accumulated, UnknownToInput<Tuple[Index]>]
110+
>;
111+
50112
export const string: Schema<string>;
51113
export const boolean: Schema<boolean>;
52114
export const integer: Schema<number>;
@@ -108,6 +170,7 @@ export function literal(value: undefined): Schema<undefined>;
108170
export function literal(value: null): Schema<null>;
109171
export function literal<T>(value: T): Schema<T>;
110172

173+
// TODO: Deprecate for V9
111174
export function tuple(schemas: []): Schema<[]>;
112175
export function tuple<Output, Input>(
113176
schemas: [Schema<Output, Input>]
@@ -161,33 +224,13 @@ export const jsonString: <Output>(
161224
space?: number
162225
) => Schema<Output, string>;
163226

164-
export const union: <A extends UnknownSchema, B extends UnknownSchema[]>(
227+
export const union: <A, B extends unknown[]>(
165228
schemas: [A, ...B]
166229
) => Schema<
167-
Output<A> | SchemaTupleOutput<B>[number],
168-
Input<A> | SchemaTupleInput<B>[number]
230+
UnknownToOuput<A> | SchemaUnionTupleOutput<B>[number],
231+
UnknownToInput<A> | SchemaUnionTupleInput<B>[number]
169232
>;
170233

171-
type UnknownToOuput<T> = T extends Schema<unknown>
172-
? Output<T>
173-
: T extends {
174-
[k in keyof T]: unknown;
175-
}
176-
? {
177-
[k in keyof T]: UnknownToOuput<T[k]>;
178-
}
179-
: T;
180-
181-
type UnknownToInput<T> = T extends Schema<unknown>
182-
? Input<T>
183-
: T extends {
184-
[k in keyof T]: unknown;
185-
}
186-
? {
187-
[k in keyof T]: UnknownToInput<T[k]>;
188-
}
189-
: T;
190-
191234
/**
192235
* @deprecated Pass the Schema directly instead of using the s.matches method
193236
*/
@@ -214,6 +257,9 @@ export function object<Output>(
214257
tag: (name: string, value: unknown) => void;
215258
}) => Output
216259
): Schema<Output, unknown>;
260+
/**
261+
* @deprecated Will be removed in V9. Use S.schema instead
262+
*/
217263
export function object<
218264
Shape extends {
219265
[k in keyof Shape]: unknown;
@@ -388,6 +434,9 @@ export const trim: <Input>(
388434

389435
export type UnknownKeys = "Strip" | "Strict";
390436

437+
/**
438+
* @deprecated Will be removed in V9
439+
*/
391440
export function unwrap<Value>(result: Result<Value>): Value;
392441

393442
export type GlobalConfigOverride = {

src/S.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const nullish = S.nullable;
1616
export const array = S.array;
1717
export const record = S.dict;
1818
export const jsonString = S.jsonString;
19-
export const union = S.union;
19+
export const union = S.js_union;
2020
export const object = S.js_object;
2121
export const schema = S.schema;
2222
export const safe = S.js_safe;

src/S_Core.bs.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3008,6 +3008,10 @@ function unwrap(result) {
30083008
throw result._0;
30093009
}
30103010

3011+
function js_union(values) {
3012+
return factory$7(values.map(definitionToSchema));
3013+
}
3014+
30113015
function js_transform(schema, maybeParser, maybeSerializer) {
30123016
return transform$1(schema, (function (s) {
30133017
return {
@@ -3413,6 +3417,7 @@ export {
34133417
trim ,
34143418
js_safe ,
34153419
js_safeAsync ,
3420+
js_union ,
34163421
js_parseAsyncWith ,
34173422
js_optional ,
34183423
js_tuple ,

src/S_Core.res

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4152,6 +4152,8 @@ let unwrap = result => {
41524152
// JS/TS API
41534153
// =============
41544154

4155+
let js_union = values => Union.factory(values->Js.Array2.map(Schema.definitionToSchema))
4156+
41554157
let js_transform = (schema, ~parser as maybeParser=?, ~serializer as maybeSerializer=?) => {
41564158
schema->transform(s => {
41574159
{

src/S_Core.resi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,8 @@ type jsResult<'value>
421421
let js_safe: (unit => 'v) => jsResult<'v>
422422
let js_safeAsync: (unit => promise<'v>) => promise<jsResult<'v>>
423423

424+
let js_union: array<unknown> => t<'value>
425+
424426
// TODO: Replace parseAsyncWith with it in V9
425427
let js_parseAsyncWith: (Js.Json.t, t<'value>) => promise<'value>
426428

0 commit comments

Comments
 (0)