Skip to content

Commit e3a8be2

Browse files
committed
[union] Allow non-empty type arrays
1 parent 92cb7ed commit e3a8be2

11 files changed

Lines changed: 111 additions & 56 deletions

File tree

site/guides/05_schemas/1_using_schemas.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ rejected by the schema.
5757

5858
## Allowing A Union Of Types
5959

60-
Use an array of two or more type names when a Cell or Value can have more than
61-
one broad type:
60+
Use a non-empty array of type names to list the broad types that a Cell or Value
61+
can have:
6262

6363
```js
6464
const unionStore = createStore().setValuesSchema({
@@ -70,9 +70,10 @@ console.log(unionStore.getValues());
7070
// -> {reference: 42, response: {accepted: true}}
7171
```
7272

73-
A type union can contain `string`, `number`, `boolean`, `object`, and `array`.
73+
A type array can contain `string`, `number`, `boolean`, `object`, and `array`.
74+
Multiple type names form a union, and repeated names have no additional effect.
7475
Do not include `null` in the array: use `allowNull: true` instead. A default is
75-
used only when it matches one of the union's types, or is `null` when null is
76+
used only when it matches one of the array's types, or is `null` when null is
7677
allowed.
7778

7879
## Restricting Values With Enums

site/guides/20_releases.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ supported primitive enum and literal constraints in the schemas they produce.
3737

3838
## Native Schema Type Unions
3939

40-
CellSchema and ValueSchema can now accept an array in the `type` property when
41-
a Cell or Value can have more than one broad type:
40+
CellSchema and ValueSchema can now accept a non-empty array in the `type`
41+
property to list one or more allowed broad types:
4242

4343
```js
4444
const unionStore = createStore().setValuesSchema({
@@ -52,11 +52,12 @@ console.log(unionStore.getValues());
5252
// -> {reference: 'unknown', response: {accepted: true}}
5353
```
5454

55-
A type union must contain at least two of `string`, `number`, `boolean`,
56-
`object`, and `array`. `null` continues to be represented by `allowNull: true`,
57-
and defaults are used only when they match one of the listed types. Each schema
58-
entry still uses exactly one of `type` or `enum`: a type union allows every
59-
value of its listed types, while an enum allows only its listed values.
55+
A type array can contain `string`, `number`, `boolean`, `object`, and `array`.
56+
Multiple type names form a union, and repeated names have no additional effect.
57+
`null` continues to be represented by `allowNull: true`, and defaults are used
58+
only when they match one of the listed types. Each schema entry still uses
59+
exactly one of `type` or `enum`: a type array allows every value of its listed
60+
types, while an enum allows only its listed values.
6061

6162
The schema-aware Store APIs infer the corresponding TypeScript unions. The
6263
ArkType, Effect Schema, TypeBox, Valibot, and Zod schematizers also preserve

src/@types/schematizers/docs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
* type, default value, nullable flag, and optional required flag. It should
5353
* recursively unwrap optional/nullable wrappers and return a tuple of
5454
* [schema, defaultValue, allowNull, required]. The unwrapped schema should have
55-
* either a supported `type` property, a `type` array containing two or more
55+
* either a supported `type` property, a non-empty `type` array containing
5656
* supported types, or a non-empty `enum` property containing primitive values.
5757
* @param getProperties - A function that extracts the properties/entries/shape
5858
* from an object schema. Returns undefined if the schema is not an object.

src/@types/store/docs.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@
4242
* Table.
4343
*
4444
* A CellSchema specifies either the type of the Cell (`string`, `boolean`,
45-
* `number`, `object`, or `array`), a union of two or more of those types, or a
46-
* non-empty `enum` of exact primitive values that are allowed. Use an array for
47-
* a type union, such as `type: ['string', 'number']`. The `type` and `enum`
48-
* properties are mutually exclusive, and enum members can be strings, finite
49-
* numbers, or booleans, including a mixture of those types.
45+
* `number`, `object`, or `array`), a non-empty array of those types, or a
46+
* non-empty `enum` of exact primitive values that are allowed. Multiple type
47+
* names form a union, such as `type: ['string', 'number']`, and repeated names
48+
* have no additional effect. The `type` and `enum` properties are mutually
49+
* exclusive, and enum members can be strings, finite numbers, or booleans,
50+
* including a mixture of those types.
5051
*
5152
* For `object` and `array` types, TinyBase automatically serializes values to
5253
* and from JSON when storing and retrieving them. Their contents should
@@ -154,11 +155,12 @@
154155
* Store.
155156
*
156157
* A ValueSchema specifies either the type of the Value (`string`, `boolean`,
157-
* `number`, `object`, or `array`), a union of two or more of those types, or a
158-
* non-empty `enum` of exact primitive values that are allowed. Use an array for
159-
* a type union, such as `type: ['string', 'number']`. The `type` and `enum`
160-
* properties are mutually exclusive, and enum members can be strings, finite
161-
* numbers, or booleans, including a mixture of those types.
158+
* `number`, `object`, or `array`), a non-empty array of those types, or a
159+
* non-empty `enum` of exact primitive values that are allowed. Multiple type
160+
* names form a union, such as `type: ['string', 'number']`, and repeated names
161+
* have no additional effect. The `type` and `enum` properties are mutually
162+
* exclusive, and enum members can be strings, finite numbers, or booleans,
163+
* including a mixture of those types.
162164
*
163165
* For `object` and `array` types, TinyBase automatically serializes values to
164166
* and from JSON when storing and retrieving them. Their contents should

src/@types/store/index.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212

1313
type SchemaType = 'string' | 'number' | 'boolean' | 'object' | 'array';
1414

15-
type SchemaTypeUnion = readonly [SchemaType, SchemaType, ...SchemaType[]];
15+
type SchemaTypeArray = readonly [SchemaType, ...SchemaType[]];
1616

1717
/// TablesSchema
1818
export type TablesSchema = {[tableId: Id]: {[cellId: Id]: CellSchema}};
@@ -55,7 +55,7 @@ export type CellSchema =
5555
enum?: never;
5656
}
5757
| {
58-
type: SchemaTypeUnion;
58+
type: SchemaTypeArray;
5959
default?: string | number | boolean | AnyObject | AnyArray | null;
6060
allowNull?: boolean;
6161
required?: boolean;
@@ -113,7 +113,7 @@ export type ValueSchema =
113113
enum?: never;
114114
}
115115
| {
116-
type: SchemaTypeUnion;
116+
type: SchemaTypeArray;
117117
default?: string | number | boolean | AnyObject | AnyArray | null;
118118
allowNull?: boolean;
119119
required?: boolean;

src/@types/store/with-schemas/index.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import type {
2525

2626
type SchemaType = 'string' | 'number' | 'boolean' | 'object' | 'array';
2727

28-
type SchemaTypeUnion = readonly [SchemaType, SchemaType, ...SchemaType[]];
28+
type SchemaTypeArray = readonly [SchemaType, ...SchemaType[]];
2929

3030
type CellOrValueFromSchemaType<Type> = Type extends readonly (infer Type)[]
3131
? CellOrValueFromSchemaType<Type>
@@ -82,7 +82,7 @@ export type CellSchema =
8282
enum?: never;
8383
}
8484
| {
85-
type: SchemaTypeUnion;
85+
type: SchemaTypeArray;
8686
default?: string | number | boolean | AnyObject | AnyArray | null;
8787
allowNull?: boolean;
8888
required?: boolean;
@@ -140,7 +140,7 @@ export type ValueSchema =
140140
enum?: never;
141141
}
142142
| {
143-
type: SchemaTypeUnion;
143+
type: SchemaTypeArray;
144144
default?: string | number | boolean | AnyObject | AnyArray | null;
145145
allowNull?: boolean;
146146
required?: boolean;

src/common/cell.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,7 @@ export type CellOrValueSchemaType = Exclude<CellOrValueType, 'null'>;
4040

4141
export type CellOrValueSchemaTypes =
4242
| CellOrValueSchemaType
43-
| readonly [
44-
CellOrValueSchemaType,
45-
CellOrValueSchemaType,
46-
...CellOrValueSchemaType[],
47-
];
43+
| readonly [CellOrValueSchemaType, ...CellOrValueSchemaType[]];
4844

4945
export const getCellOrValueType = (
5046
cellOrValue: any,
@@ -80,7 +76,7 @@ export const isCellOrValueSchemaTypes = (
8076
types: any,
8177
): types is CellOrValueSchemaTypes =>
8278
isArray(types)
83-
? !isUndefined(types[1]) && arrayEvery(types, isCellOrValueSchemaType)
79+
? !isUndefined(types[0]) && arrayEvery(types, isCellOrValueSchemaType)
8480
: isCellOrValueSchemaType(types);
8581

8682
export const cellOrValueSchemaTypeIncludes = (

test/unit/core/store/store-schema.test.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,8 @@ describe.each([
278278
answer: {type: ['string', 'number'], default: 'unknown'},
279279
payload: {type: ['object', 'array'], required: true},
280280
score: {type: ['number', 'boolean'], allowNull: true},
281+
single: {type: ['string']},
282+
repeated: {type: ['string', 'string']},
281283
status: {enum: ['draft', 'live']},
282284
},
283285
} as const;
@@ -287,33 +289,45 @@ describe.each([
287289
store.setRow('pets', 'pet1', {
288290
payload: {likes: ['walks']},
289291
score: null,
292+
single: 'initial',
293+
repeated: 'initial',
290294
status: 'draft',
291295
});
292296
expect(store.getRow('pets', 'pet1')).toEqual({
293297
answer: 'unknown',
294298
payload: {likes: ['walks']},
295299
score: null,
300+
single: 'initial',
301+
repeated: 'initial',
296302
status: 'draft',
297303
});
298304

299305
store.setCell('pets', 'pet1', 'answer', 42);
300306
store.setCell('pets', 'pet1', 'payload', ['walks']);
301307
store.setCell('pets', 'pet1', 'score', true);
308+
store.setCell('pets', 'pet1', 'single', 'one');
309+
store.setCell('pets', 'pet1', 'repeated', 'two');
302310
expect(store.getRow('pets', 'pet1')).toEqual({
303311
answer: 42,
304312
payload: ['walks'],
305313
score: true,
314+
single: 'one',
315+
repeated: 'two',
306316
status: 'draft',
307317
});
308318

309319
store.setCell('pets', 'pet1', 'answer', false);
310320
store.setCell('pets', 'pet1', 'payload', null);
311321
store.setCell('pets', 'pet1', 'score', 'high');
322+
store.setCell('pets', 'pet1', 'single', 1);
323+
store.setCell('pets', 'pet1', 'repeated', false);
312324
store.setCell('pets', 'pet1', 'status', 'other');
313325
expect(store.getRow('pets', 'pet1')).toEqual({
314326
answer: 'unknown',
315327
payload: ['walks'],
316328
score: true,
329+
single: 'one',
330+
repeated: 'two',
317331
status: 'draft',
318332
});
319333
expectChanges(
@@ -322,6 +336,8 @@ describe.each([
322336
{pets: {pet1: {answer: [false]}}},
323337
{pets: {pet1: {payload: [null]}}},
324338
{pets: {pet1: {score: ['high']}}},
339+
{pets: {pet1: {single: [1]}}},
340+
{pets: {pet1: {repeated: [false]}}},
325341
{pets: {pet1: {status: ['other']}}},
326342
);
327343

@@ -342,7 +358,6 @@ describe.each([
342358

343359
for (const answer of [
344360
{type: []},
345-
{type: ['string']},
346361
{type: ['string', 'null']},
347362
{type: ['string', 'date']},
348363
{type: ['string', 'boolean'], enum: ['draft']},
@@ -579,6 +594,8 @@ describe.each([
579594
answer: {type: ['string', 'number'], default: 'unknown'},
580595
payload: {type: ['object', 'array'], required: true},
581596
score: {type: ['number', 'boolean'], allowNull: true},
597+
single: {type: ['string']},
598+
repeated: {type: ['string', 'string']},
582599
status: {enum: ['draft', 'live']},
583600
} as const;
584601
store.setValuesSchema(valuesSchema);
@@ -587,40 +604,54 @@ describe.each([
587604
'invalids',
588605
{payload: [undefined]},
589606
{score: [undefined]},
607+
{single: [undefined]},
608+
{repeated: [undefined]},
590609
{status: [undefined]},
591610
);
592611
expect(JSON.parse(store.getValuesSchemaJson())).toEqual(valuesSchema);
593612

594613
store.setValues({
595614
payload: {likes: ['walks']},
596615
score: null,
616+
single: 'initial',
617+
repeated: 'initial',
597618
status: 'draft',
598619
});
599620
expect(store.getValues()).toEqual({
600621
answer: 'unknown',
601622
payload: {likes: ['walks']},
602623
score: null,
624+
single: 'initial',
625+
repeated: 'initial',
603626
status: 'draft',
604627
});
605628

606629
store.setValue('answer', 42);
607630
store.setValue('payload', ['walks']);
608631
store.setValue('score', true);
632+
store.setValue('single', 'one');
633+
store.setValue('repeated', 'two');
609634
expect(store.getValues()).toEqual({
610635
answer: 42,
611636
payload: ['walks'],
612637
score: true,
638+
single: 'one',
639+
repeated: 'two',
613640
status: 'draft',
614641
});
615642

616643
store.setValue('answer', false);
617644
store.setValue('payload', null);
618645
store.setValue('score', 'high');
646+
store.setValue('single', 1);
647+
store.setValue('repeated', false);
619648
store.setValue('status', 'other');
620649
expect(store.getValues()).toEqual({
621650
answer: 'unknown',
622651
payload: ['walks'],
623652
score: true,
653+
single: 'one',
654+
repeated: 'two',
624655
status: 'draft',
625656
});
626657
expectChanges(
@@ -629,6 +660,8 @@ describe.each([
629660
{answer: [false]},
630661
{payload: [null]},
631662
{score: ['high']},
663+
{single: [1]},
664+
{repeated: [false]},
632665
{status: ['other']},
633666
);
634667

@@ -647,7 +680,6 @@ describe.each([
647680

648681
for (const answer of [
649682
{type: []},
650-
{type: ['string']},
651683
{type: ['string', 'null']},
652684
{type: ['string', 'date']},
653685
{type: ['string', 'boolean'], enum: ['draft']},

0 commit comments

Comments
 (0)