Skip to content

Commit 970d102

Browse files
authored
feat: add exactOptional support in type struct (#38)
Add support of `exactOptional` with `superstruct.type`. We plan to use `@metamask/superstruct` in `@metamask/keyring-api` and we already use the combo `type` + `exactOptional` (our implementation) there. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, localized change to `type()` entry iteration aligned with existing `object()` semantics; covered by new tests with no auth or data-path impact. > > **Overview** > Extends **`type()`** so **`exactOptional()`** fields behave like TypeScript’s optional properties: the key may be **missing**, but an explicit **`undefined`** value is still validated and rejected. > > The **`type()`** `entries` generator now skips properties wrapped in **`exactOptional`** when the key is **not** on the object (`hasOwnProperty`), matching the existing **`object()`** behavior. > > Adds typing coverage for **`type` + `exactOptional`**, plus validation fixtures for absent keys, present values, and **`age: undefined`** failing validation. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e49b9ff. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 597569d commit 970d102

5 files changed

Lines changed: 67 additions & 0 deletions

File tree

src/structs/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,12 @@ export function type<Schema extends ObjectSchema>(
684684
*entries(value) {
685685
if (isObject(value)) {
686686
for (const k of keys) {
687+
if (
688+
ExactOptionalStruct.isExactOptional(schema[k]) &&
689+
!Object.prototype.hasOwnProperty.call(value, k)
690+
) {
691+
continue;
692+
}
687693
yield [k, value[k], schema[k] as Struct<any>];
688694
}
689695
}

test/typings/exactOptional.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
string,
55
number,
66
object,
7+
type,
78
enums,
89
never,
910
record,
@@ -60,3 +61,17 @@ test<{
6061
);
6162
return value;
6263
});
64+
65+
test<{
66+
a?: number;
67+
b: string;
68+
}>((value) => {
69+
assert(
70+
value,
71+
type({
72+
a: exactOptional(number()),
73+
b: string(),
74+
}),
75+
);
76+
return value;
77+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { type, exactOptional, string, number } from '../../../src';
2+
3+
export const Struct = type({
4+
name: string(),
5+
age: exactOptional(number()),
6+
});
7+
8+
export const data = {
9+
name: 'john',
10+
age: undefined,
11+
};
12+
13+
export const failures = [
14+
{
15+
value: undefined,
16+
type: 'number',
17+
refinement: undefined,
18+
path: ['age'],
19+
branch: [data, data.age],
20+
},
21+
];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { type, exactOptional, string, number } from '../../../src';
2+
3+
export const Struct = type({
4+
name: string(),
5+
age: exactOptional(number()),
6+
});
7+
8+
export const data = {
9+
name: 'john',
10+
};
11+
12+
export const output = data;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { type, exactOptional, string, number } from '../../../src';
2+
3+
export const Struct = type({
4+
name: string(),
5+
age: exactOptional(number()),
6+
});
7+
8+
export const data = {
9+
name: 'john',
10+
age: 42,
11+
};
12+
13+
export const output = data;

0 commit comments

Comments
 (0)