typesea 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -6
- package/README.md +143 -28
- package/dist/adapters/index.d.ts +50 -8
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +169 -48
- package/dist/aot/index.d.ts +19 -3
- package/dist/aot/index.d.ts.map +1 -1
- package/dist/aot/index.js +115 -17
- package/dist/async/index.d.ts +28 -56
- package/dist/async/index.d.ts.map +1 -1
- package/dist/async/index.js +94 -37
- package/dist/builders/composite.d.ts +43 -9
- package/dist/builders/composite.d.ts.map +1 -1
- package/dist/builders/composite.js +100 -17
- package/dist/builders/index.d.ts +8 -5
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +7 -4
- package/dist/builders/modifier.d.ts +36 -5
- package/dist/builders/modifier.d.ts.map +1 -1
- package/dist/builders/modifier.js +52 -5
- package/dist/builders/object/guard.d.ts +72 -24
- package/dist/builders/object/guard.d.ts.map +1 -1
- package/dist/builders/object/guard.js +139 -29
- package/dist/builders/object/index.d.ts +4 -2
- package/dist/builders/object/index.d.ts.map +1 -1
- package/dist/builders/object/index.js +3 -1
- package/dist/builders/object/schema.d.ts +88 -11
- package/dist/builders/object/schema.d.ts.map +1 -1
- package/dist/builders/object/schema.js +290 -23
- package/dist/builders/object/types.d.ts +20 -31
- package/dist/builders/object/types.d.ts.map +1 -1
- package/dist/builders/object/types.js +2 -0
- package/dist/builders/runtime.d.ts +40 -0
- package/dist/builders/runtime.d.ts.map +1 -0
- package/dist/builders/runtime.js +150 -0
- package/dist/builders/scalar.d.ts +49 -9
- package/dist/builders/scalar.d.ts.map +1 -1
- package/dist/builders/scalar.js +87 -9
- package/dist/builders/table.d.ts +35 -5
- package/dist/builders/table.d.ts.map +1 -1
- package/dist/builders/table.js +35 -5
- package/dist/builders/types.d.ts +20 -4
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +2 -0
- package/dist/compile/check-composite.d.ts +25 -2
- package/dist/compile/check-composite.d.ts.map +1 -1
- package/dist/compile/check-composite.js +699 -27
- package/dist/compile/check-scalar.d.ts +88 -0
- package/dist/compile/check-scalar.d.ts.map +1 -1
- package/dist/compile/check-scalar.js +570 -3
- package/dist/compile/check.d.ts +12 -0
- package/dist/compile/check.d.ts.map +1 -1
- package/dist/compile/check.js +62 -3
- package/dist/compile/context.d.ts +47 -9
- package/dist/compile/context.d.ts.map +1 -1
- package/dist/compile/context.js +53 -8
- package/dist/compile/first.d.ts +26 -0
- package/dist/compile/first.d.ts.map +1 -0
- package/dist/compile/first.js +850 -0
- package/dist/compile/graph-predicate.d.ts +4 -2
- package/dist/compile/graph-predicate.d.ts.map +1 -1
- package/dist/compile/graph-predicate.js +2272 -165
- package/dist/compile/guard.d.ts +16 -24
- package/dist/compile/guard.d.ts.map +1 -1
- package/dist/compile/guard.js +202 -72
- package/dist/compile/index.d.ts +3 -1
- package/dist/compile/index.d.ts.map +1 -1
- package/dist/compile/index.js +2 -0
- package/dist/compile/issue.d.ts +110 -0
- package/dist/compile/issue.d.ts.map +1 -1
- package/dist/compile/issue.js +184 -1
- package/dist/compile/names.d.ts +12 -2
- package/dist/compile/names.d.ts.map +1 -1
- package/dist/compile/names.js +19 -3
- package/dist/compile/predicate.d.ts +24 -0
- package/dist/compile/predicate.d.ts.map +1 -1
- package/dist/compile/predicate.js +287 -10
- package/dist/compile/runtime.d.ts +100 -13
- package/dist/compile/runtime.d.ts.map +1 -1
- package/dist/compile/runtime.js +56 -6
- package/dist/compile/source.d.ts +10 -2
- package/dist/compile/source.d.ts.map +1 -1
- package/dist/compile/source.js +385 -26
- package/dist/compile/types.d.ts +22 -0
- package/dist/compile/types.d.ts.map +1 -1
- package/dist/compile/types.js +2 -0
- package/dist/decoder/index.d.ts +92 -46
- package/dist/decoder/index.d.ts.map +1 -1
- package/dist/decoder/index.js +266 -39
- package/dist/evaluate/check-composite.d.ts +111 -2
- package/dist/evaluate/check-composite.d.ts.map +1 -1
- package/dist/evaluate/check-composite.js +343 -8
- package/dist/evaluate/check-scalar.d.ts +25 -0
- package/dist/evaluate/check-scalar.d.ts.map +1 -1
- package/dist/evaluate/check-scalar.js +124 -3
- package/dist/evaluate/check.d.ts +7 -0
- package/dist/evaluate/check.d.ts.map +1 -1
- package/dist/evaluate/check.js +62 -4
- package/dist/evaluate/index.d.ts +2 -0
- package/dist/evaluate/index.d.ts.map +1 -1
- package/dist/evaluate/index.js +2 -0
- package/dist/evaluate/issue.d.ts +11 -1
- package/dist/evaluate/issue.d.ts.map +1 -1
- package/dist/evaluate/issue.js +15 -1
- package/dist/evaluate/predicate.d.ts +16 -5
- package/dist/evaluate/predicate.d.ts.map +1 -1
- package/dist/evaluate/predicate.js +20 -5
- package/dist/evaluate/shared.d.ts +78 -13
- package/dist/evaluate/shared.d.ts.map +1 -1
- package/dist/evaluate/shared.js +101 -8
- package/dist/evaluate/state.d.ts +35 -13
- package/dist/evaluate/state.d.ts.map +1 -1
- package/dist/evaluate/state.js +35 -2
- package/dist/guard/array.d.ts +48 -0
- package/dist/guard/array.d.ts.map +1 -0
- package/dist/guard/array.js +84 -0
- package/dist/guard/base.d.ts +111 -31
- package/dist/guard/base.d.ts.map +1 -1
- package/dist/guard/base.js +165 -32
- package/dist/guard/date.d.ts +34 -0
- package/dist/guard/date.d.ts.map +1 -0
- package/dist/guard/date.js +60 -0
- package/dist/guard/error.d.ts +10 -5
- package/dist/guard/error.d.ts.map +1 -1
- package/dist/guard/error.js +10 -5
- package/dist/guard/index.d.ts +4 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +4 -0
- package/dist/guard/number.d.ts +86 -11
- package/dist/guard/number.d.ts.map +1 -1
- package/dist/guard/number.js +159 -11
- package/dist/guard/props.d.ts +27 -3
- package/dist/guard/props.d.ts.map +1 -1
- package/dist/guard/props.js +27 -3
- package/dist/guard/read.d.ts +115 -10
- package/dist/guard/read.d.ts.map +1 -1
- package/dist/guard/read.js +185 -10
- package/dist/guard/registry.d.ts +12 -2
- package/dist/guard/registry.d.ts.map +1 -1
- package/dist/guard/registry.js +15 -3
- package/dist/guard/string.d.ts +115 -13
- package/dist/guard/string.d.ts.map +1 -1
- package/dist/guard/string.js +250 -13
- package/dist/guard/types.d.ts +110 -40
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/guard/types.js +2 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/internal/index.d.ts +42 -6
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +51 -8
- package/dist/ir/builder.d.ts +17 -127
- package/dist/ir/builder.d.ts.map +1 -1
- package/dist/ir/builder.js +80 -137
- package/dist/ir/freeze.d.ts +4 -0
- package/dist/ir/freeze.d.ts.map +1 -1
- package/dist/ir/freeze.js +66 -0
- package/dist/ir/index.d.ts +3 -1
- package/dist/ir/index.d.ts.map +1 -1
- package/dist/ir/index.js +2 -0
- package/dist/ir/regexp.d.ts +2 -0
- package/dist/ir/regexp.d.ts.map +1 -1
- package/dist/ir/regexp.js +2 -0
- package/dist/ir/types.d.ts +94 -56
- package/dist/ir/types.d.ts.map +1 -1
- package/dist/ir/types.js +2 -0
- package/dist/ir/validate.d.ts +8 -1
- package/dist/ir/validate.d.ts.map +1 -1
- package/dist/ir/validate.js +511 -61
- package/dist/issue/index.d.ts +42 -10
- package/dist/issue/index.d.ts.map +1 -1
- package/dist/issue/index.js +65 -11
- package/dist/json-schema/emit-combinator.d.ts +44 -4
- package/dist/json-schema/emit-combinator.d.ts.map +1 -1
- package/dist/json-schema/emit-combinator.js +44 -4
- package/dist/json-schema/emit-composite.d.ts +16 -2
- package/dist/json-schema/emit-composite.d.ts.map +1 -1
- package/dist/json-schema/emit-composite.js +81 -13
- package/dist/json-schema/emit-scalar.d.ts +26 -3
- package/dist/json-schema/emit-scalar.d.ts.map +1 -1
- package/dist/json-schema/emit-scalar.js +124 -10
- package/dist/json-schema/emit-types.d.ts +11 -1
- package/dist/json-schema/emit-types.d.ts.map +1 -1
- package/dist/json-schema/emit-types.js +2 -0
- package/dist/json-schema/emit.d.ts +12 -1
- package/dist/json-schema/emit.d.ts.map +1 -1
- package/dist/json-schema/emit.js +23 -3
- package/dist/json-schema/freeze.d.ts +13 -2
- package/dist/json-schema/freeze.d.ts.map +1 -1
- package/dist/json-schema/freeze.js +41 -8
- package/dist/json-schema/index.d.ts +16 -2
- package/dist/json-schema/index.d.ts.map +1 -1
- package/dist/json-schema/index.js +23 -3
- package/dist/json-schema/issue.d.ts +4 -1
- package/dist/json-schema/issue.d.ts.map +1 -1
- package/dist/json-schema/issue.js +4 -1
- package/dist/json-schema/read.d.ts +24 -3
- package/dist/json-schema/read.d.ts.map +1 -1
- package/dist/json-schema/read.js +59 -12
- package/dist/json-schema/types.d.ts +45 -16
- package/dist/json-schema/types.d.ts.map +1 -1
- package/dist/json-schema/types.js +2 -0
- package/dist/kind/index.d.ts +40 -28
- package/dist/kind/index.d.ts.map +1 -1
- package/dist/kind/index.js +41 -13
- package/dist/lower/index.d.ts +6 -1
- package/dist/lower/index.d.ts.map +1 -1
- package/dist/lower/index.js +462 -46
- package/dist/message/index.d.ts +64 -10
- package/dist/message/index.d.ts.map +1 -1
- package/dist/message/index.js +155 -17
- package/dist/optimize/algebraic.d.ts +54 -0
- package/dist/optimize/algebraic.d.ts.map +1 -0
- package/dist/optimize/algebraic.js +314 -0
- package/dist/optimize/compact.d.ts +8 -1
- package/dist/optimize/compact.d.ts.map +1 -1
- package/dist/optimize/compact.js +13 -2
- package/dist/optimize/domain.d.ts +16 -0
- package/dist/optimize/domain.d.ts.map +1 -0
- package/dist/optimize/domain.js +619 -0
- package/dist/optimize/fold-boolean.d.ts +17 -2
- package/dist/optimize/fold-boolean.d.ts.map +1 -1
- package/dist/optimize/fold-boolean.js +59 -14
- package/dist/optimize/fold-common.d.ts +43 -8
- package/dist/optimize/fold-common.d.ts.map +1 -1
- package/dist/optimize/fold-common.js +37 -6
- package/dist/optimize/fold-constraints.d.ts +33 -0
- package/dist/optimize/fold-constraints.d.ts.map +1 -0
- package/dist/optimize/fold-constraints.js +484 -0
- package/dist/optimize/fold-scalar.d.ts +98 -13
- package/dist/optimize/fold-scalar.d.ts.map +1 -1
- package/dist/optimize/fold-scalar.js +98 -13
- package/dist/optimize/fold.d.ts +8 -1
- package/dist/optimize/fold.d.ts.map +1 -1
- package/dist/optimize/fold.js +22 -2
- package/dist/optimize/index.d.ts +9 -1
- package/dist/optimize/index.d.ts.map +1 -1
- package/dist/optimize/index.js +18 -3
- package/dist/optimize/map-node.d.ts +3 -1
- package/dist/optimize/map-node.d.ts.map +1 -1
- package/dist/optimize/map-node.js +48 -3
- package/dist/optimize/peephole.d.ts +16 -0
- package/dist/optimize/peephole.d.ts.map +1 -0
- package/dist/optimize/peephole.js +254 -0
- package/dist/optimize/remap.d.ts +2 -0
- package/dist/optimize/remap.d.ts.map +1 -1
- package/dist/optimize/remap.js +2 -0
- package/dist/optimize/rewrite.d.ts +13 -8
- package/dist/optimize/rewrite.d.ts.map +1 -1
- package/dist/optimize/rewrite.js +13 -8
- package/dist/plan/cache.d.ts +9 -3
- package/dist/plan/cache.d.ts.map +1 -1
- package/dist/plan/cache.js +34 -6
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.d.ts.map +1 -1
- package/dist/plan/index.js +2 -0
- package/dist/plan/predicate.d.ts +2 -0
- package/dist/plan/predicate.d.ts.map +1 -1
- package/dist/plan/predicate.js +298 -29
- package/dist/plan/schema-predicate.d.ts +6 -0
- package/dist/plan/schema-predicate.d.ts.map +1 -1
- package/dist/plan/schema-predicate.js +382 -19
- package/dist/plan/types.d.ts +2 -0
- package/dist/plan/types.d.ts.map +1 -1
- package/dist/plan/types.js +2 -0
- package/dist/result/index.d.ts +19 -5
- package/dist/result/index.d.ts.map +1 -1
- package/dist/result/index.js +10 -2
- package/dist/schema/common.d.ts +69 -6
- package/dist/schema/common.d.ts.map +1 -1
- package/dist/schema/common.js +104 -10
- package/dist/schema/freeze.d.ts +4 -0
- package/dist/schema/freeze.d.ts.map +1 -1
- package/dist/schema/freeze.js +40 -0
- package/dist/schema/index.d.ts +5 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/lazy.d.ts +4 -0
- package/dist/schema/lazy.d.ts.map +1 -1
- package/dist/schema/lazy.js +4 -0
- package/dist/schema/literal.d.ts +7 -1
- package/dist/schema/literal.d.ts.map +1 -1
- package/dist/schema/literal.js +7 -1
- package/dist/schema/types.d.ts +109 -100
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +13 -2
- package/dist/schema/undefined.d.ts +17 -0
- package/dist/schema/undefined.d.ts.map +1 -0
- package/dist/schema/undefined.js +77 -0
- package/dist/schema/validate.d.ts +8 -1
- package/dist/schema/validate.d.ts.map +1 -1
- package/dist/schema/validate.js +255 -57
- package/docs/api.md +128 -8
- package/docs/assets/benchmark-headline.svg +163 -0
- package/docs/engine-notes.md +62 -15
- package/docs/index.html +1340 -702
- package/docs/ko/api.md +375 -0
- package/docs/ko/engine-notes.md +156 -0
- package/docs/ko/readme.md +378 -0
- package/package.json +66 -65
package/docs/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta
|
|
7
7
|
name="description"
|
|
8
|
-
content="TypeSea documentation:
|
|
8
|
+
content="TypeSea documentation: complete README, API reference, and engine notes for zero-dependency TypeScript runtime validation."
|
|
9
9
|
>
|
|
10
10
|
<title>TypeSea Docs</title>
|
|
11
11
|
<style>
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
--code-comment: #8b98a9;
|
|
30
30
|
--sidebar-bg: #1d2422;
|
|
31
31
|
--sidebar-ink: #e9efe9;
|
|
32
|
-
--sidebar-muted: #9fae a5;
|
|
33
32
|
--sidebar-muted: #9faea5;
|
|
34
33
|
--sidebar-line: rgba(233, 239, 233, 0.12);
|
|
35
34
|
--shadow: 0 10px 30px rgba(28, 36, 34, 0.07);
|
|
@@ -70,16 +69,9 @@
|
|
|
70
69
|
margin: 0;
|
|
71
70
|
background: var(--bg);
|
|
72
71
|
color: var(--ink);
|
|
73
|
-
font-family:
|
|
74
|
-
ui-sans-serif,
|
|
75
|
-
system-ui,
|
|
76
|
-
-apple-system,
|
|
77
|
-
"Segoe UI",
|
|
78
|
-
Roboto,
|
|
79
|
-
"Helvetica Neue",
|
|
80
|
-
sans-serif;
|
|
72
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
81
73
|
font-size: 15px;
|
|
82
|
-
line-height: 1.
|
|
74
|
+
line-height: 1.62;
|
|
83
75
|
}
|
|
84
76
|
|
|
85
77
|
a {
|
|
@@ -89,13 +81,7 @@
|
|
|
89
81
|
|
|
90
82
|
code,
|
|
91
83
|
pre {
|
|
92
|
-
font-family:
|
|
93
|
-
ui-monospace,
|
|
94
|
-
"SFMono-Regular",
|
|
95
|
-
Menlo,
|
|
96
|
-
Consolas,
|
|
97
|
-
"Liberation Mono",
|
|
98
|
-
monospace;
|
|
84
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
|
|
99
85
|
}
|
|
100
86
|
|
|
101
87
|
p code,
|
|
@@ -107,12 +93,18 @@
|
|
|
107
93
|
font-size: 0.92em;
|
|
108
94
|
}
|
|
109
95
|
|
|
110
|
-
|
|
96
|
+
.language-radio {
|
|
97
|
+
position: absolute;
|
|
98
|
+
width: 1px;
|
|
99
|
+
height: 1px;
|
|
100
|
+
opacity: 0;
|
|
101
|
+
pointer-events: none;
|
|
102
|
+
}
|
|
111
103
|
|
|
112
104
|
.shell {
|
|
113
105
|
display: grid;
|
|
114
106
|
min-height: 100vh;
|
|
115
|
-
grid-template-columns:
|
|
107
|
+
grid-template-columns: 284px minmax(0, 1fr);
|
|
116
108
|
}
|
|
117
109
|
|
|
118
110
|
.sidebar {
|
|
@@ -138,7 +130,6 @@
|
|
|
138
130
|
margin: 0;
|
|
139
131
|
font-size: 22px;
|
|
140
132
|
line-height: 1;
|
|
141
|
-
letter-spacing: -0.01em;
|
|
142
133
|
}
|
|
143
134
|
|
|
144
135
|
.brand .version {
|
|
@@ -156,37 +147,11 @@
|
|
|
156
147
|
border-bottom: 1px solid var(--sidebar-line);
|
|
157
148
|
color: var(--sidebar-muted);
|
|
158
149
|
font-size: 12.5px;
|
|
159
|
-
line-height: 1.5;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.search {
|
|
163
|
-
padding: 16px 16px 8px;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.search input {
|
|
167
|
-
width: 100%;
|
|
168
|
-
min-height: 36px;
|
|
169
|
-
border: 1px solid var(--sidebar-line);
|
|
170
|
-
border-radius: 8px;
|
|
171
|
-
padding: 0 12px;
|
|
172
|
-
background: rgba(255, 255, 255, 0.06);
|
|
173
|
-
color: var(--sidebar-ink);
|
|
174
|
-
font: inherit;
|
|
175
|
-
font-size: 13px;
|
|
176
|
-
outline: none;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
.search input:focus {
|
|
180
|
-
border-color: #46b48f;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.search input::placeholder {
|
|
184
|
-
color: var(--sidebar-muted);
|
|
185
150
|
}
|
|
186
151
|
|
|
187
152
|
.nav {
|
|
188
153
|
flex: 1;
|
|
189
|
-
padding:
|
|
154
|
+
padding: 8px 12px 18px;
|
|
190
155
|
}
|
|
191
156
|
|
|
192
157
|
.nav-group {
|
|
@@ -202,14 +167,14 @@
|
|
|
202
167
|
.nav a {
|
|
203
168
|
display: flex;
|
|
204
169
|
align-items: center;
|
|
205
|
-
min-height:
|
|
170
|
+
min-height: 32px;
|
|
206
171
|
border-left: 2px solid transparent;
|
|
207
172
|
border-radius: 0 7px 7px 0;
|
|
208
173
|
margin: 1px 0;
|
|
209
174
|
padding: 5px 12px;
|
|
210
175
|
color: var(--sidebar-ink);
|
|
211
|
-
opacity: 0.
|
|
212
|
-
font-size:
|
|
176
|
+
opacity: 0.84;
|
|
177
|
+
font-size: 13px;
|
|
213
178
|
}
|
|
214
179
|
|
|
215
180
|
.nav a:hover,
|
|
@@ -218,13 +183,6 @@
|
|
|
218
183
|
opacity: 1;
|
|
219
184
|
}
|
|
220
185
|
|
|
221
|
-
.nav a.active {
|
|
222
|
-
border-left-color: #34c496;
|
|
223
|
-
background: rgba(52, 196, 150, 0.1);
|
|
224
|
-
color: #9ce8cd;
|
|
225
|
-
opacity: 1;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
186
|
.sidebar-foot {
|
|
229
187
|
padding: 14px 22px 20px;
|
|
230
188
|
border-top: 1px solid var(--sidebar-line);
|
|
@@ -232,8 +190,6 @@
|
|
|
232
190
|
font-size: 12px;
|
|
233
191
|
}
|
|
234
192
|
|
|
235
|
-
/* ---------- content ---------- */
|
|
236
|
-
|
|
237
193
|
.content {
|
|
238
194
|
min-width: 0;
|
|
239
195
|
}
|
|
@@ -263,13 +219,17 @@
|
|
|
263
219
|
font-weight: 650;
|
|
264
220
|
}
|
|
265
221
|
|
|
222
|
+
.topbar-actions,
|
|
266
223
|
.topbar-links {
|
|
267
224
|
display: flex;
|
|
225
|
+
align-items: center;
|
|
268
226
|
flex-wrap: wrap;
|
|
227
|
+
justify-content: flex-end;
|
|
269
228
|
gap: 8px;
|
|
270
229
|
}
|
|
271
230
|
|
|
272
|
-
.topbar-links a
|
|
231
|
+
.topbar-links a,
|
|
232
|
+
.language-switch label {
|
|
273
233
|
border: 1px solid var(--line);
|
|
274
234
|
border-radius: 7px;
|
|
275
235
|
padding: 5px 11px;
|
|
@@ -278,31 +238,58 @@
|
|
|
278
238
|
font-size: 12.5px;
|
|
279
239
|
}
|
|
280
240
|
|
|
281
|
-
.
|
|
241
|
+
.language-switch {
|
|
242
|
+
display: inline-flex;
|
|
243
|
+
gap: 4px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.language-switch label {
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
font-weight: 700;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#language-en:checked ~ .shell label[for="language-en"],
|
|
252
|
+
#language-ko:checked ~ .shell label[for="language-ko"] {
|
|
282
253
|
border-color: var(--accent);
|
|
254
|
+
background: var(--accent-soft);
|
|
283
255
|
color: var(--accent-ink);
|
|
284
256
|
}
|
|
285
257
|
|
|
258
|
+
.locale-ko,
|
|
259
|
+
.i18n-ko {
|
|
260
|
+
display: none;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#language-ko:checked ~ .shell .locale-en,
|
|
264
|
+
#language-ko:checked ~ .shell .i18n-en {
|
|
265
|
+
display: none;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#language-ko:checked ~ .shell .locale-ko {
|
|
269
|
+
display: block;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#language-ko:checked ~ .shell .i18n-ko {
|
|
273
|
+
display: inline;
|
|
274
|
+
}
|
|
275
|
+
|
|
286
276
|
main {
|
|
287
|
-
max-width:
|
|
277
|
+
max-width: 980px;
|
|
288
278
|
margin: 0 auto;
|
|
289
|
-
padding: 40px 34px
|
|
279
|
+
padding: 40px 34px 84px;
|
|
290
280
|
}
|
|
291
281
|
|
|
292
|
-
section
|
|
293
|
-
|
|
282
|
+
section,
|
|
283
|
+
article.source-doc {
|
|
284
|
+
padding: 30px 0 36px;
|
|
294
285
|
border-bottom: 1px solid var(--line);
|
|
295
|
-
scroll-margin-top:
|
|
286
|
+
scroll-margin-top: 74px;
|
|
296
287
|
}
|
|
297
288
|
|
|
298
289
|
section:first-child {
|
|
299
290
|
padding-top: 0;
|
|
300
291
|
}
|
|
301
292
|
|
|
302
|
-
section:last-child {
|
|
303
|
-
border-bottom: 0;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
293
|
.eyebrow {
|
|
307
294
|
margin: 0 0 6px;
|
|
308
295
|
color: var(--accent);
|
|
@@ -314,72 +301,46 @@
|
|
|
314
301
|
|
|
315
302
|
h2 {
|
|
316
303
|
margin: 0 0 10px;
|
|
317
|
-
font-size:
|
|
304
|
+
font-size: 26px;
|
|
318
305
|
line-height: 1.2;
|
|
319
|
-
letter-spacing: -0.01em;
|
|
320
306
|
}
|
|
321
307
|
|
|
322
308
|
h3 {
|
|
323
|
-
margin:
|
|
324
|
-
font-size:
|
|
309
|
+
margin: 24px 0 8px;
|
|
310
|
+
font-size: 18px;
|
|
325
311
|
line-height: 1.3;
|
|
326
312
|
}
|
|
327
313
|
|
|
314
|
+
h4 {
|
|
315
|
+
margin: 20px 0 8px;
|
|
316
|
+
font-size: 15px;
|
|
317
|
+
}
|
|
318
|
+
|
|
328
319
|
p {
|
|
329
320
|
margin: 0 0 12px;
|
|
330
321
|
}
|
|
331
322
|
|
|
332
323
|
.lede {
|
|
333
|
-
max-width:
|
|
324
|
+
max-width: 760px;
|
|
334
325
|
margin-bottom: 16px;
|
|
335
326
|
color: var(--muted);
|
|
336
327
|
font-size: 15.5px;
|
|
337
328
|
}
|
|
338
329
|
|
|
339
|
-
/* ---------- components ---------- */
|
|
340
|
-
|
|
341
330
|
.hero {
|
|
342
331
|
display: grid;
|
|
343
|
-
grid-template-columns: minmax(0, 1fr)
|
|
332
|
+
grid-template-columns: minmax(0, 1fr) 310px;
|
|
344
333
|
gap: 28px;
|
|
345
334
|
align-items: start;
|
|
346
335
|
}
|
|
347
336
|
|
|
348
|
-
.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
margin: 16px 0 0;
|
|
352
|
-
padding: 0;
|
|
353
|
-
list-style: none;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
.checklist li {
|
|
357
|
-
position: relative;
|
|
358
|
-
padding-left: 24px;
|
|
359
|
-
color: var(--muted);
|
|
360
|
-
font-size: 14px;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
.checklist li::before {
|
|
364
|
-
position: absolute;
|
|
365
|
-
left: 0;
|
|
366
|
-
top: 1px;
|
|
367
|
-
color: var(--accent);
|
|
368
|
-
font-weight: 700;
|
|
369
|
-
content: "✓";
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
.checklist li strong {
|
|
373
|
-
color: var(--ink);
|
|
374
|
-
font-weight: 600;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
.status-panel {
|
|
337
|
+
.status-panel,
|
|
338
|
+
.tile,
|
|
339
|
+
.admonition {
|
|
378
340
|
border: 1px solid var(--line);
|
|
379
341
|
border-radius: 10px;
|
|
380
342
|
background: var(--surface);
|
|
381
343
|
box-shadow: var(--shadow);
|
|
382
|
-
overflow: hidden;
|
|
383
344
|
}
|
|
384
345
|
|
|
385
346
|
.status-panel header {
|
|
@@ -410,76 +371,13 @@
|
|
|
410
371
|
align-items: center;
|
|
411
372
|
border-radius: 999px;
|
|
412
373
|
padding: 2px 9px;
|
|
374
|
+
background: var(--accent-soft);
|
|
375
|
+
color: var(--accent-ink);
|
|
413
376
|
font-size: 11.5px;
|
|
414
377
|
font-weight: 700;
|
|
415
378
|
white-space: nowrap;
|
|
416
379
|
}
|
|
417
380
|
|
|
418
|
-
.pill.green {
|
|
419
|
-
background: var(--accent-soft);
|
|
420
|
-
color: var(--accent-ink);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
.pill.amber {
|
|
424
|
-
background: var(--amber-soft);
|
|
425
|
-
color: var(--amber);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
.pill.violet {
|
|
429
|
-
background: var(--violet-soft);
|
|
430
|
-
color: var(--violet);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
.callout {
|
|
434
|
-
display: grid;
|
|
435
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
436
|
-
gap: 10px;
|
|
437
|
-
margin: 16px 0 0;
|
|
438
|
-
border: 1px solid var(--line);
|
|
439
|
-
border-left: 3px solid var(--accent);
|
|
440
|
-
border-radius: 8px;
|
|
441
|
-
padding: 12px 14px;
|
|
442
|
-
background: var(--surface);
|
|
443
|
-
font-size: 13.5px;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
.callout.warn {
|
|
447
|
-
border-left-color: var(--amber);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
.callout .icon {
|
|
451
|
-
font-size: 15px;
|
|
452
|
-
line-height: 1.5;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
.callout p {
|
|
456
|
-
margin: 0;
|
|
457
|
-
color: var(--muted);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
.callout strong {
|
|
461
|
-
color: var(--ink);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
.code-block {
|
|
465
|
-
margin: 16px 0 0;
|
|
466
|
-
border-radius: 10px;
|
|
467
|
-
overflow-x: auto;
|
|
468
|
-
background: var(--code-bg);
|
|
469
|
-
color: var(--code-ink);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
.code-block pre {
|
|
473
|
-
margin: 0;
|
|
474
|
-
padding: 16px 18px;
|
|
475
|
-
font-size: 13px;
|
|
476
|
-
line-height: 1.6;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
.code-block .cm {
|
|
480
|
-
color: var(--code-comment);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
381
|
.grid {
|
|
484
382
|
display: grid;
|
|
485
383
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
@@ -492,10 +390,12 @@
|
|
|
492
390
|
}
|
|
493
391
|
|
|
494
392
|
.tile {
|
|
495
|
-
border: 1px solid var(--line);
|
|
496
|
-
border-radius: 10px;
|
|
497
393
|
padding: 15px 16px;
|
|
498
|
-
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.tile h3 {
|
|
397
|
+
margin-top: 0;
|
|
398
|
+
font-size: 14.5px;
|
|
499
399
|
}
|
|
500
400
|
|
|
501
401
|
.tile p {
|
|
@@ -504,74 +404,70 @@
|
|
|
504
404
|
font-size: 13px;
|
|
505
405
|
}
|
|
506
406
|
|
|
507
|
-
.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
padding:
|
|
512
|
-
|
|
513
|
-
color: var(--accent-ink);
|
|
514
|
-
font-size: 12.5px;
|
|
407
|
+
.checklist {
|
|
408
|
+
display: grid;
|
|
409
|
+
gap: 8px;
|
|
410
|
+
margin: 16px 0 0;
|
|
411
|
+
padding: 0;
|
|
412
|
+
list-style: none;
|
|
515
413
|
}
|
|
516
414
|
|
|
517
|
-
.
|
|
518
|
-
|
|
415
|
+
.checklist li {
|
|
416
|
+
position: relative;
|
|
417
|
+
padding-left: 24px;
|
|
418
|
+
color: var(--muted);
|
|
419
|
+
font-size: 14px;
|
|
519
420
|
}
|
|
520
421
|
|
|
521
|
-
.
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
422
|
+
.checklist li::before {
|
|
423
|
+
position: absolute;
|
|
424
|
+
left: 0;
|
|
425
|
+
top: 1px;
|
|
426
|
+
color: var(--accent);
|
|
427
|
+
font-weight: 700;
|
|
428
|
+
content: "✓";
|
|
527
429
|
}
|
|
528
430
|
|
|
529
|
-
.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
border-radius: 9px;
|
|
534
|
-
padding: 12px 13px 11px;
|
|
535
|
-
background: var(--surface);
|
|
536
|
-
counter-increment: stage;
|
|
431
|
+
.doc-content a {
|
|
432
|
+
color: var(--accent-ink);
|
|
433
|
+
text-decoration: underline;
|
|
434
|
+
text-underline-offset: 2px;
|
|
537
435
|
}
|
|
538
436
|
|
|
539
|
-
.
|
|
540
|
-
|
|
437
|
+
.doc-content ul,
|
|
438
|
+
.doc-content ol {
|
|
439
|
+
margin: 0 0 14px;
|
|
440
|
+
padding-left: 24px;
|
|
541
441
|
}
|
|
542
442
|
|
|
543
|
-
.
|
|
544
|
-
|
|
545
|
-
margin-bottom: 4px;
|
|
546
|
-
color: var(--muted);
|
|
547
|
-
font-size: 10.5px;
|
|
548
|
-
font-weight: 700;
|
|
549
|
-
letter-spacing: 0.06em;
|
|
550
|
-
content: "STEP " counter(stage);
|
|
443
|
+
.doc-content li {
|
|
444
|
+
margin: 4px 0;
|
|
551
445
|
}
|
|
552
446
|
|
|
553
|
-
.
|
|
554
|
-
|
|
555
|
-
margin
|
|
556
|
-
|
|
447
|
+
.code-block,
|
|
448
|
+
.doc-content pre {
|
|
449
|
+
margin: 16px 0;
|
|
450
|
+
border-radius: 10px;
|
|
451
|
+
overflow-x: auto;
|
|
452
|
+
background: var(--code-bg);
|
|
453
|
+
color: var(--code-ink);
|
|
557
454
|
}
|
|
558
455
|
|
|
559
|
-
.
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
456
|
+
.code-block pre,
|
|
457
|
+
.doc-content pre {
|
|
458
|
+
padding: 16px 18px;
|
|
459
|
+
font-size: 13px;
|
|
460
|
+
line-height: 1.6;
|
|
564
461
|
}
|
|
565
462
|
|
|
566
|
-
.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
margin-top: 12px;
|
|
463
|
+
.doc-content pre code {
|
|
464
|
+
background: transparent;
|
|
465
|
+
padding: 0;
|
|
466
|
+
color: inherit;
|
|
571
467
|
}
|
|
572
468
|
|
|
573
469
|
.table-wrap {
|
|
574
|
-
margin
|
|
470
|
+
margin: 16px 0;
|
|
575
471
|
border: 1px solid var(--line);
|
|
576
472
|
border-radius: 10px;
|
|
577
473
|
background: var(--surface);
|
|
@@ -594,11 +490,11 @@
|
|
|
594
490
|
|
|
595
491
|
th {
|
|
596
492
|
background: var(--surface-2);
|
|
493
|
+
color: var(--muted);
|
|
597
494
|
font-size: 12px;
|
|
598
495
|
font-weight: 700;
|
|
599
496
|
letter-spacing: 0.03em;
|
|
600
497
|
text-transform: uppercase;
|
|
601
|
-
color: var(--muted);
|
|
602
498
|
white-space: nowrap;
|
|
603
499
|
}
|
|
604
500
|
|
|
@@ -606,34 +502,59 @@
|
|
|
606
502
|
border-bottom: 0;
|
|
607
503
|
}
|
|
608
504
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
505
|
+
.admonition {
|
|
506
|
+
margin: 16px 0;
|
|
507
|
+
border-left: 3px solid var(--accent);
|
|
508
|
+
padding: 13px 15px;
|
|
509
|
+
box-shadow: none;
|
|
613
510
|
}
|
|
614
511
|
|
|
615
|
-
|
|
616
|
-
|
|
512
|
+
.admonition.warning,
|
|
513
|
+
.admonition.caution {
|
|
514
|
+
border-left-color: var(--amber);
|
|
617
515
|
}
|
|
618
516
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
517
|
+
.admonition-title {
|
|
518
|
+
display: block;
|
|
519
|
+
margin-bottom: 5px;
|
|
520
|
+
color: var(--ink);
|
|
521
|
+
font-size: 13px;
|
|
522
|
+
font-weight: 750;
|
|
622
523
|
}
|
|
623
524
|
|
|
624
|
-
.
|
|
625
|
-
margin-
|
|
525
|
+
.admonition p:last-child {
|
|
526
|
+
margin-bottom: 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.source-doc > header {
|
|
530
|
+
margin-bottom: 18px;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.source-doc > header p {
|
|
626
534
|
color: var(--muted);
|
|
627
|
-
font-size:
|
|
535
|
+
font-size: 13.5px;
|
|
628
536
|
}
|
|
629
537
|
|
|
630
|
-
.
|
|
631
|
-
display:
|
|
538
|
+
.benchmark-image {
|
|
539
|
+
display: block;
|
|
540
|
+
max-width: 100%;
|
|
541
|
+
height: auto;
|
|
542
|
+
margin: 16px 0;
|
|
543
|
+
border-radius: 10px;
|
|
632
544
|
}
|
|
633
545
|
|
|
634
|
-
|
|
546
|
+
.muted-link {
|
|
547
|
+
color: var(--muted);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
hr {
|
|
551
|
+
height: 1px;
|
|
552
|
+
border: 0;
|
|
553
|
+
margin: 28px 0;
|
|
554
|
+
background: var(--line);
|
|
555
|
+
}
|
|
635
556
|
|
|
636
|
-
@media (max-width:
|
|
557
|
+
@media (max-width: 980px) {
|
|
637
558
|
.shell {
|
|
638
559
|
grid-template-columns: 1fr;
|
|
639
560
|
}
|
|
@@ -648,16 +569,12 @@
|
|
|
648
569
|
}
|
|
649
570
|
|
|
650
571
|
.grid,
|
|
651
|
-
.
|
|
652
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
.pipeline {
|
|
572
|
+
.grid.two {
|
|
656
573
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
657
574
|
}
|
|
658
575
|
}
|
|
659
576
|
|
|
660
|
-
@media (max-width:
|
|
577
|
+
@media (max-width: 640px) {
|
|
661
578
|
.topbar {
|
|
662
579
|
position: static;
|
|
663
580
|
flex-direction: column;
|
|
@@ -665,118 +582,195 @@
|
|
|
665
582
|
padding: 12px 18px;
|
|
666
583
|
}
|
|
667
584
|
|
|
668
|
-
|
|
669
|
-
|
|
585
|
+
.topbar-actions {
|
|
586
|
+
justify-content: flex-start;
|
|
670
587
|
}
|
|
671
588
|
|
|
672
|
-
|
|
673
|
-
|
|
589
|
+
main {
|
|
590
|
+
padding: 28px 18px 56px;
|
|
674
591
|
}
|
|
675
592
|
|
|
676
593
|
.grid,
|
|
677
|
-
.grid.two
|
|
678
|
-
.consumers,
|
|
679
|
-
.pipeline {
|
|
594
|
+
.grid.two {
|
|
680
595
|
grid-template-columns: 1fr;
|
|
681
596
|
}
|
|
682
597
|
}
|
|
683
598
|
</style>
|
|
684
599
|
</head>
|
|
685
600
|
<body>
|
|
601
|
+
<input class="language-radio" type="radio" name="docs-language" id="language-en" checked>
|
|
602
|
+
<input class="language-radio" type="radio" name="docs-language" id="language-ko">
|
|
686
603
|
<div class="shell">
|
|
687
604
|
<aside class="sidebar" aria-label="Documentation navigation">
|
|
688
605
|
<div class="brand">
|
|
689
606
|
<h1>TypeSea</h1>
|
|
690
|
-
<span class="version">v0.
|
|
607
|
+
<span class="version">v0.3.0</span>
|
|
691
608
|
</div>
|
|
692
609
|
<p class="brand-tagline">
|
|
693
|
-
|
|
694
|
-
|
|
610
|
+
<span class="i18n-en">Complete docs from README.md, docs/api.md, and docs/engine-notes.md.</span>
|
|
611
|
+
<span class="i18n-ko" lang="ko">README, API 레퍼런스, 엔진 노트를 한 곳에 모은 문서입니다.</span>
|
|
695
612
|
</p>
|
|
696
|
-
<div class="search">
|
|
697
|
-
<input data-search type="search" placeholder="Filter sections…" aria-label="Filter documentation sections">
|
|
698
|
-
</div>
|
|
699
613
|
<nav class="nav">
|
|
700
|
-
<p class="nav-group">
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
<
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
<
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
614
|
+
<p class="nav-group">
|
|
615
|
+
<span class="i18n-en">Start</span>
|
|
616
|
+
<span class="i18n-ko" lang="ko">시작</span>
|
|
617
|
+
</p>
|
|
618
|
+
<a href="#overview">
|
|
619
|
+
<span class="i18n-en">Overview</span>
|
|
620
|
+
<span class="i18n-ko" lang="ko">개요</span>
|
|
621
|
+
</a>
|
|
622
|
+
<a href="#quick-start">
|
|
623
|
+
<span class="i18n-en">Quick start</span>
|
|
624
|
+
<span class="i18n-ko" lang="ko">빠른 시작</span>
|
|
625
|
+
</a>
|
|
626
|
+
<a href="#architecture">
|
|
627
|
+
<span class="i18n-en">Architecture</span>
|
|
628
|
+
<span class="i18n-ko" lang="ko">아키텍처</span>
|
|
629
|
+
</a>
|
|
630
|
+
<a href="#api">
|
|
631
|
+
<span class="i18n-en">API map</span>
|
|
632
|
+
<span class="i18n-ko" lang="ko">API 지도</span>
|
|
633
|
+
</a>
|
|
634
|
+
<a href="#adapters">
|
|
635
|
+
<span class="i18n-en">Adapters</span>
|
|
636
|
+
<span class="i18n-ko" lang="ko">어댑터</span>
|
|
637
|
+
</a>
|
|
638
|
+
<a href="#benchmarks">
|
|
639
|
+
<span class="i18n-en">Benchmarks</span>
|
|
640
|
+
<span class="i18n-ko" lang="ko">벤치마크</span>
|
|
641
|
+
</a>
|
|
642
|
+
<a href="#release">
|
|
643
|
+
<span class="i18n-en">Release gate</span>
|
|
644
|
+
<span class="i18n-ko" lang="ko">릴리스 게이트</span>
|
|
645
|
+
</a>
|
|
646
|
+
<a href="#files">
|
|
647
|
+
<span class="i18n-en">Source files</span>
|
|
648
|
+
<span class="i18n-ko" lang="ko">원본 파일</span>
|
|
649
|
+
</a>
|
|
650
|
+
<p class="nav-group">
|
|
651
|
+
<span class="i18n-en">Full Documents</span>
|
|
652
|
+
<span class="i18n-ko" lang="ko">전체 문서</span>
|
|
653
|
+
</p>
|
|
654
|
+
<a href="#readme">README</a>
|
|
655
|
+
<a href="#api-reference">
|
|
656
|
+
<span class="i18n-en">API Reference</span>
|
|
657
|
+
<span class="i18n-ko" lang="ko">API 레퍼런스</span>
|
|
658
|
+
</a>
|
|
659
|
+
<a href="#engine-notes">
|
|
660
|
+
<span class="i18n-en">Engine Notes</span>
|
|
661
|
+
<span class="i18n-ko" lang="ko">엔진 노트</span>
|
|
662
|
+
</a>
|
|
712
663
|
</nav>
|
|
713
|
-
<p class="sidebar-foot">
|
|
664
|
+
<p class="sidebar-foot">
|
|
665
|
+
<span class="i18n-en">MIT License · zero runtime dependencies</span>
|
|
666
|
+
<span class="i18n-ko" lang="ko">MIT License · 런타임 의존성 없음</span>
|
|
667
|
+
</p>
|
|
714
668
|
</aside>
|
|
715
669
|
|
|
716
670
|
<div class="content">
|
|
717
671
|
<div class="topbar">
|
|
718
|
-
<span class="topbar-crumb"
|
|
719
|
-
|
|
720
|
-
<
|
|
721
|
-
<
|
|
722
|
-
|
|
672
|
+
<span class="topbar-crumb">
|
|
673
|
+
<strong>TypeSea</strong> /
|
|
674
|
+
<span class="i18n-en">Documentation</span>
|
|
675
|
+
<span class="i18n-ko" lang="ko">문서</span>
|
|
676
|
+
</span>
|
|
677
|
+
<div class="topbar-actions">
|
|
678
|
+
<div class="language-switch" aria-label="Documentation language">
|
|
679
|
+
<label for="language-en">EN</label>
|
|
680
|
+
<label for="language-ko">한국어</label>
|
|
681
|
+
</div>
|
|
682
|
+
<div class="topbar-links">
|
|
683
|
+
<a href="https://github.com/Feralthedogg/TypeSea">GitHub</a>
|
|
684
|
+
<a href="https://github.com/Feralthedogg/TypeSea/blob/main/docs/api.md">
|
|
685
|
+
<span class="i18n-en">API reference</span>
|
|
686
|
+
<span class="i18n-ko" lang="ko">API 레퍼런스</span>
|
|
687
|
+
</a>
|
|
688
|
+
<a href="https://github.com/Feralthedogg/TypeSea/blob/main/docs/engine-notes.md">
|
|
689
|
+
<span class="i18n-en">Engine notes</span>
|
|
690
|
+
<span class="i18n-ko" lang="ko">엔진 노트</span>
|
|
691
|
+
</a>
|
|
692
|
+
</div>
|
|
723
693
|
</div>
|
|
724
694
|
</div>
|
|
725
695
|
|
|
726
696
|
<main>
|
|
727
|
-
<section id="overview" data-doc-section
|
|
697
|
+
<section id="overview" data-doc-section>
|
|
728
698
|
<div class="hero">
|
|
729
699
|
<div>
|
|
730
|
-
<p class="eyebrow">
|
|
731
|
-
|
|
700
|
+
<p class="eyebrow">
|
|
701
|
+
<span class="i18n-en">Overview</span>
|
|
702
|
+
<span class="i18n-ko" lang="ko">개요</span>
|
|
703
|
+
</p>
|
|
704
|
+
<h2>
|
|
705
|
+
<span class="i18n-en">Zero-dependency TypeScript validation with compiled type guards.</span>
|
|
706
|
+
<span class="i18n-ko" lang="ko">컴파일된 타입 가드까지 제공하는, 런타임 의존성 없는 TypeScript 검증 라이브러리.</span>
|
|
707
|
+
</h2>
|
|
732
708
|
<p class="lede">
|
|
733
|
-
TypeSea
|
|
734
|
-
|
|
735
|
-
optimized graph — and executes through the plan, a runtime-compiled
|
|
736
|
-
validator, or a standalone AOT module.
|
|
709
|
+
<span class="i18n-en">TypeSea turns immutable schemas into runtime guards, compiled validators, AOT modules, JSON Schema exports, framework adapters, and frozen diagnostic Result values.</span>
|
|
710
|
+
<span class="i18n-ko" lang="ko">TypeSea는 불변 스키마를 런타임 가드, 컴파일된 검증기, AOT 모듈, JSON Schema 출력, 프레임워크 어댑터, 동결된 진단 Result로 연결합니다.</span>
|
|
737
711
|
</p>
|
|
738
712
|
<ul class="checklist">
|
|
739
|
-
<li
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
<li
|
|
713
|
+
<li>
|
|
714
|
+
<span class="i18n-en"><strong>Zero dependencies</strong> — enforced before release.</span>
|
|
715
|
+
<span class="i18n-ko" lang="ko"><strong>의존성 없음</strong> — 릴리스 전에 정책 게이트로 강제합니다.</span>
|
|
716
|
+
</li>
|
|
717
|
+
<li>
|
|
718
|
+
<span class="i18n-en"><strong>Safe by default</strong> — descriptor reads avoid getter execution.</span>
|
|
719
|
+
<span class="i18n-ko" lang="ko"><strong>기본값은 안전 모드</strong> — descriptor read로 getter 실행을 피합니다.</span>
|
|
720
|
+
</li>
|
|
721
|
+
<li>
|
|
722
|
+
<span class="i18n-en"><strong>Fast when trusted</strong> — unsafe and unchecked modes trade hardening for direct reads.</span>
|
|
723
|
+
<span class="i18n-ko" lang="ko"><strong>신뢰된 데이터에서는 빠르게</strong> — unsafe/unchecked 모드는 일부 방어를 direct read 성능과 맞바꿉니다.</span>
|
|
724
|
+
</li>
|
|
725
|
+
<li>
|
|
726
|
+
<span class="i18n-en"><strong>Complete references</strong> — README, API reference, and engine notes are rendered below.</span>
|
|
727
|
+
<span class="i18n-ko" lang="ko"><strong>문서 전체 포함</strong> — README, API 레퍼런스, 엔진 노트 전문을 아래에서 볼 수 있습니다.</span>
|
|
728
|
+
</li>
|
|
744
729
|
</ul>
|
|
745
730
|
</div>
|
|
746
731
|
<aside class="status-panel" aria-label="Package status">
|
|
747
|
-
<header>
|
|
732
|
+
<header>
|
|
733
|
+
<span class="i18n-en">Package status</span>
|
|
734
|
+
<span class="i18n-ko" lang="ko">패키지 상태</span>
|
|
735
|
+
</header>
|
|
748
736
|
<div class="status-row">
|
|
749
|
-
<span>
|
|
750
|
-
|
|
737
|
+
<span>
|
|
738
|
+
<span class="i18n-en">Runtime dependencies</span>
|
|
739
|
+
<span class="i18n-ko" lang="ko">런타임 의존성</span>
|
|
740
|
+
</span>
|
|
741
|
+
<span class="pill">zero</span>
|
|
751
742
|
</div>
|
|
752
743
|
<div class="status-row">
|
|
753
|
-
<span>
|
|
754
|
-
|
|
744
|
+
<span>
|
|
745
|
+
<span class="i18n-en">Execution paths</span>
|
|
746
|
+
<span class="i18n-ko" lang="ko">실행 경로</span>
|
|
747
|
+
</span>
|
|
748
|
+
<span class="pill">plan · jit · aot</span>
|
|
755
749
|
</div>
|
|
756
750
|
<div class="status-row">
|
|
757
|
-
<span>
|
|
758
|
-
|
|
751
|
+
<span>
|
|
752
|
+
<span class="i18n-en">Module format</span>
|
|
753
|
+
<span class="i18n-ko" lang="ko">모듈 형식</span>
|
|
754
|
+
</span>
|
|
755
|
+
<span class="pill">ESM-only</span>
|
|
759
756
|
</div>
|
|
760
757
|
<div class="status-row">
|
|
761
758
|
<span>Node.js</span>
|
|
762
|
-
<span class="pill
|
|
763
|
-
</div>
|
|
764
|
-
<div class="status-row">
|
|
765
|
-
<span>Adapters</span>
|
|
766
|
-
<span class="pill amber">structural</span>
|
|
759
|
+
<span class="pill">≥ 20.19</span>
|
|
767
760
|
</div>
|
|
768
761
|
</aside>
|
|
769
762
|
</div>
|
|
770
763
|
</section>
|
|
771
764
|
|
|
772
|
-
<section id="quick-start" data-doc-section
|
|
773
|
-
<p class="eyebrow">
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
The same guard narrows through <code>is()</code>, returns immutable diagnostics
|
|
777
|
-
through <code>check()</code>, exports lossless JSON Schema, or compiles into a
|
|
778
|
-
generated validator.
|
|
765
|
+
<section id="quick-start" data-doc-section>
|
|
766
|
+
<p class="eyebrow">
|
|
767
|
+
<span class="i18n-en">Quick start</span>
|
|
768
|
+
<span class="i18n-ko" lang="ko">빠른 시작</span>
|
|
779
769
|
</p>
|
|
770
|
+
<h2>
|
|
771
|
+
<span class="i18n-en">Install, define, narrow, compile.</span>
|
|
772
|
+
<span class="i18n-ko" lang="ko">설치하고, 스키마를 정의하고, 타입을 좁힌 뒤, 필요한 곳만 컴파일하세요.</span>
|
|
773
|
+
</h2>
|
|
780
774
|
<div class="code-block">
|
|
781
775
|
<pre><code>npm install typesea</code></pre>
|
|
782
776
|
</div>
|
|
@@ -791,452 +785,1096 @@ const User = t.strictObject({
|
|
|
791
785
|
|
|
792
786
|
type User = Infer<typeof User>;
|
|
793
787
|
|
|
794
|
-
<span class="cm">// 1) Boolean narrowing — no diagnostic allocation on success</span>
|
|
795
788
|
if (User.is(input)) {
|
|
796
|
-
input.id;
|
|
789
|
+
input.id;
|
|
797
790
|
}
|
|
798
791
|
|
|
799
|
-
<span class="cm">// 2) Immutable diagnostics — frozen Result, never throws</span>
|
|
800
792
|
const checked = User.check(input);
|
|
801
|
-
|
|
802
|
-
<span class="cm">// 3) Hot path — generated validator code</span>
|
|
803
793
|
const FastUser = compile(User, { name: "isUser" });
|
|
804
|
-
|
|
805
|
-
<span class="cm">// 4) Interop — lossless-only JSON Schema export</span>
|
|
806
794
|
const schema = toJsonSchema(User);</code></pre>
|
|
807
795
|
</div>
|
|
808
|
-
<div class="callout warn">
|
|
809
|
-
<span class="icon">⚠</span>
|
|
810
|
-
<p>
|
|
811
|
-
<strong><code>compile()</code> uses <code>new Function</code></strong>, which throws under a
|
|
812
|
-
Content-Security-Policy that forbids <code>unsafe-eval</code>. In CSP-restricted
|
|
813
|
-
environments, emit validator source ahead of time with <code>emitAotModule()</code>.
|
|
814
|
-
</p>
|
|
815
|
-
</div>
|
|
816
796
|
</section>
|
|
817
797
|
|
|
818
|
-
<section id="
|
|
819
|
-
<p class="eyebrow">
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
Object presence is a first-class concept, and edge cases that break typical
|
|
823
|
-
validators are deliberate, documented decisions pinned by tests.
|
|
798
|
+
<section id="architecture" data-doc-section>
|
|
799
|
+
<p class="eyebrow">
|
|
800
|
+
<span class="i18n-en">Architecture</span>
|
|
801
|
+
<span class="i18n-ko" lang="ko">아키텍처</span>
|
|
824
802
|
</p>
|
|
825
|
-
<
|
|
826
|
-
<
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
<th>Wrapper</th>
|
|
830
|
-
<th>Key may be absent</th>
|
|
831
|
-
<th>Value may be <code>undefined</code></th>
|
|
832
|
-
<th>Inferred type</th>
|
|
833
|
-
</tr>
|
|
834
|
-
</thead>
|
|
835
|
-
<tbody>
|
|
836
|
-
<tr>
|
|
837
|
-
<td><code>t.optional(inner)</code></td>
|
|
838
|
-
<td>yes</td>
|
|
839
|
-
<td>no</td>
|
|
840
|
-
<td><code>key?: T</code></td>
|
|
841
|
-
</tr>
|
|
842
|
-
<tr>
|
|
843
|
-
<td><code>t.undefinedable(inner)</code></td>
|
|
844
|
-
<td>no</td>
|
|
845
|
-
<td>yes</td>
|
|
846
|
-
<td><code>key: T | undefined</code></td>
|
|
847
|
-
</tr>
|
|
848
|
-
<tr>
|
|
849
|
-
<td><code>t.nullable(inner)</code></td>
|
|
850
|
-
<td>—</td>
|
|
851
|
-
<td>value may be <code>null</code></td>
|
|
852
|
-
<td><code>key: T | null</code></td>
|
|
853
|
-
</tr>
|
|
854
|
-
</tbody>
|
|
855
|
-
</table>
|
|
856
|
-
</div>
|
|
857
|
-
<div class="table-wrap">
|
|
858
|
-
<table>
|
|
859
|
-
<thead>
|
|
860
|
-
<tr>
|
|
861
|
-
<th>Input</th>
|
|
862
|
-
<th>Behavior</th>
|
|
863
|
-
</tr>
|
|
864
|
-
</thead>
|
|
865
|
-
<tbody>
|
|
866
|
-
<tr>
|
|
867
|
-
<td><code>NaN</code>, <code>Infinity</code></td>
|
|
868
|
-
<td><code>t.number</code> accepts finite numbers only; <code>t.literal(NaN)</code> matches <code>NaN</code> via <code>Object.is</code>.</td>
|
|
869
|
-
</tr>
|
|
870
|
-
<tr>
|
|
871
|
-
<td>Getter-backed properties</td>
|
|
872
|
-
<td>Never executed — reads go through descriptors, accessors count as missing data.</td>
|
|
873
|
-
</tr>
|
|
874
|
-
<tr>
|
|
875
|
-
<td><code>__proto__</code>, <code>constructor</code> keys</td>
|
|
876
|
-
<td>Validated as plain own keys through null-prototype lookups; no pollution.</td>
|
|
877
|
-
</tr>
|
|
878
|
-
<tr>
|
|
879
|
-
<td>Strict-object extras</td>
|
|
880
|
-
<td>Rejected via <code>Reflect.ownKeys</code> — including symbol keys and non-enumerable properties.</td>
|
|
881
|
-
</tr>
|
|
882
|
-
<tr>
|
|
883
|
-
<td>Sparse array holes</td>
|
|
884
|
-
<td>Read as <code>undefined</code> without executing accessors.</td>
|
|
885
|
-
</tr>
|
|
886
|
-
<tr>
|
|
887
|
-
<td>Global-flag regexes</td>
|
|
888
|
-
<td>Cloned at construction; <code>lastIndex</code> reset before every test.</td>
|
|
889
|
-
</tr>
|
|
890
|
-
<tr>
|
|
891
|
-
<td>UUID strings</td>
|
|
892
|
-
<td>RFC 9562 versions 1–8 plus the nil UUID.</td>
|
|
893
|
-
</tr>
|
|
894
|
-
<tr>
|
|
895
|
-
<td>Cyclic input values</td>
|
|
896
|
-
<td>Validate finitely via (value × schema) active-pair tracking.</td>
|
|
897
|
-
</tr>
|
|
898
|
-
<tr>
|
|
899
|
-
<td>Nesting depth</td>
|
|
900
|
-
<td>Capped at 1024 frames — deep input fails instead of overflowing the stack.</td>
|
|
901
|
-
</tr>
|
|
902
|
-
</tbody>
|
|
903
|
-
</table>
|
|
904
|
-
</div>
|
|
905
|
-
</section>
|
|
906
|
-
|
|
907
|
-
<section id="architecture" data-doc-section data-title="architecture sea of nodes ir lower optimize plan kernel graph codegen">
|
|
908
|
-
<p class="eyebrow">Architecture</p>
|
|
909
|
-
<h2>Every schema lowers into a cached validation plan.</h2>
|
|
803
|
+
<h2>
|
|
804
|
+
<span class="i18n-en">Builder -> frozen schema -> Sea-of-Nodes validation IR -> optimize -> validation plan.</span>
|
|
805
|
+
<span class="i18n-ko" lang="ko">빌더 -> 동결된 스키마 -> Sea-of-Nodes 검증 IR -> 최적화 -> 검증 계획.</span>
|
|
806
|
+
</h2>
|
|
910
807
|
<p class="lede">
|
|
911
|
-
The
|
|
912
|
-
|
|
913
|
-
as a validation plan owning both a schema-specialized predicate kernel and the
|
|
914
|
-
optimized graph consumed by generated validators.
|
|
808
|
+
<span class="i18n-en">The graph is the source for generated validators, while the plan-owned kernel keeps ordinary guard execution out of a generic node interpreter.</span>
|
|
809
|
+
<span class="i18n-ko" lang="ko">그래프는 생성 검증기의 원본이고, 계획이 소유한 커널은 일반 가드 실행이 범용 노드 인터프리터를 거치지 않게 합니다.</span>
|
|
915
810
|
</p>
|
|
916
|
-
<div class="pipeline" aria-label="TypeSea validation pipeline">
|
|
917
|
-
<div class="stage">
|
|
918
|
-
<strong>Builder</strong>
|
|
919
|
-
<span>Immutable guards validate construction inputs up front.</span>
|
|
920
|
-
</div>
|
|
921
|
-
<div class="stage">
|
|
922
|
-
<strong>Frozen schema</strong>
|
|
923
|
-
<span>The tree stores semantics and diagnostic structure.</span>
|
|
924
|
-
</div>
|
|
925
|
-
<div class="stage alt">
|
|
926
|
-
<strong>Sea-of-Nodes IR</strong>
|
|
927
|
-
<span>Lowering shares loads, predicates, keysets, and discriminant dispatch.</span>
|
|
928
|
-
</div>
|
|
929
|
-
<div class="stage alt">
|
|
930
|
-
<strong>Optimize</strong>
|
|
931
|
-
<span>Folding, reachability, and dense node ids keep invariants simple.</span>
|
|
932
|
-
</div>
|
|
933
|
-
<div class="stage">
|
|
934
|
-
<strong>Validation plan</strong>
|
|
935
|
-
<span>Cached per schema identity: predicate kernel + optimized graph.</span>
|
|
936
|
-
</div>
|
|
937
|
-
</div>
|
|
938
|
-
<div class="consumers">
|
|
939
|
-
<div class="tile">
|
|
940
|
-
<h3>Plan kernel</h3>
|
|
941
|
-
<p>Drives <code>is()</code> and the <code>check()</code> verdict preflight without per-node interpreter dispatch.</p>
|
|
942
|
-
<code>guard.is()</code>
|
|
943
|
-
</div>
|
|
944
|
-
<div class="tile">
|
|
945
|
-
<h3>Optimized graph</h3>
|
|
946
|
-
<p>Source of truth for runtime-compiled and AOT-emitted predicates, and for introspection.</p>
|
|
947
|
-
<code>compile() · emitAotModule() · graph()</code>
|
|
948
|
-
</div>
|
|
949
|
-
<div class="tile">
|
|
950
|
-
<h3>Diagnostic collector</h3>
|
|
951
|
-
<p>Failed <code>check()</code> calls replay the schema-aware collector for issue paths and codes.</p>
|
|
952
|
-
<code>guard.check()</code>
|
|
953
|
-
</div>
|
|
954
|
-
</div>
|
|
955
|
-
<div class="callout">
|
|
956
|
-
<span class="icon">🔒</span>
|
|
957
|
-
<p>
|
|
958
|
-
<strong>Injection-safe codegen:</strong> user-controlled literals, regexps, object
|
|
959
|
-
keys, keysets, and dynamic schema fallbacks live in side tables referenced by
|
|
960
|
-
numeric index. Hostile property names cannot escape into generated source —
|
|
961
|
-
pinned by dedicated injection-audit tests.
|
|
962
|
-
</p>
|
|
963
|
-
</div>
|
|
964
811
|
</section>
|
|
965
812
|
|
|
966
|
-
<section id="api" data-doc-section
|
|
967
|
-
<p class="eyebrow">
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
Every entry point is exported from the package root; builders are also grouped under
|
|
971
|
-
the <code>t</code> table. The full signatures live in the
|
|
972
|
-
<a href="https://github.com/Feralthedogg/TypeSea/blob/main/docs/api.md"><code>API reference</code></a>.
|
|
813
|
+
<section id="api" data-doc-section>
|
|
814
|
+
<p class="eyebrow">
|
|
815
|
+
<span class="i18n-en">API map</span>
|
|
816
|
+
<span class="i18n-ko" lang="ko">API 지도</span>
|
|
973
817
|
</p>
|
|
818
|
+
<h2>
|
|
819
|
+
<span class="i18n-en">Core entry points.</span>
|
|
820
|
+
<span class="i18n-ko" lang="ko">핵심 진입점.</span>
|
|
821
|
+
</h2>
|
|
974
822
|
<div class="grid">
|
|
975
823
|
<article class="tile">
|
|
976
824
|
<h3>Builders</h3>
|
|
977
|
-
<p>
|
|
978
|
-
<code>t.strictObject</code>
|
|
825
|
+
<p><code>t.string</code>, <code>t.number</code>, <code>t.object</code>, <code>t.strictObject</code>, <code>t.union</code>, <code>t.array</code>, <code>t.lazy</code>.</p>
|
|
979
826
|
</article>
|
|
980
827
|
<article class="tile">
|
|
981
|
-
<h3>
|
|
982
|
-
<p>
|
|
983
|
-
|
|
828
|
+
<h3>Validation</h3>
|
|
829
|
+
<p>
|
|
830
|
+
<span class="i18n-en"><code>is()</code> for narrowing, <code>check()</code> for Result diagnostics, <code>assert()</code> for throwing integration boundaries.</span>
|
|
831
|
+
<span class="i18n-ko" lang="ko"><code>is()</code>는 타입 좁히기, <code>check()</code>는 Result 기반 진단, <code>assert()</code>는 예외가 필요한 연동 지점에 씁니다.</span>
|
|
832
|
+
</p>
|
|
984
833
|
</article>
|
|
985
834
|
<article class="tile">
|
|
986
|
-
<h3>
|
|
987
|
-
<p>
|
|
988
|
-
|
|
835
|
+
<h3>Generated validators</h3>
|
|
836
|
+
<p>
|
|
837
|
+
<span class="i18n-en"><code>compile()</code>, <code>emitAotModule()</code>, safe mode, unsafe mode, and unchecked mode.</span>
|
|
838
|
+
<span class="i18n-ko" lang="ko"><code>compile()</code>, <code>emitAotModule()</code>, safe/unsafe/unchecked 모드.</span>
|
|
839
|
+
</p>
|
|
989
840
|
</article>
|
|
990
841
|
<article class="tile">
|
|
991
842
|
<h3>Decoders</h3>
|
|
992
|
-
<p>
|
|
993
|
-
|
|
843
|
+
<p>
|
|
844
|
+
<span class="i18n-en"><code>t.decoder</code>, <code>t.transform</code>, <code>t.pipe</code>, <code>t.coerce</code>, plus async variants.</span>
|
|
845
|
+
<span class="i18n-ko" lang="ko"><code>t.decoder</code>, <code>t.transform</code>, <code>t.pipe</code>, <code>t.coerce</code>와 async 변형.</span>
|
|
846
|
+
</p>
|
|
994
847
|
</article>
|
|
995
848
|
<article class="tile">
|
|
996
|
-
<h3>
|
|
997
|
-
<p>
|
|
998
|
-
<code>compile</code>
|
|
849
|
+
<h3>Messages</h3>
|
|
850
|
+
<p><code>formatIssue</code>, <code>formatIssues</code>, <code>withMessages</code>, <code>defineMessages</code>.</p>
|
|
999
851
|
</article>
|
|
1000
852
|
<article class="tile">
|
|
1001
|
-
<h3>
|
|
1002
|
-
<p>
|
|
1003
|
-
|
|
853
|
+
<h3>Export</h3>
|
|
854
|
+
<p>
|
|
855
|
+
<span class="i18n-en"><code>toJsonSchema</code> and <code>schemaToJsonSchema</code> succeed only when semantics are preserved.</span>
|
|
856
|
+
<span class="i18n-ko" lang="ko"><code>toJsonSchema</code>와 <code>schemaToJsonSchema</code>는 의미를 보존할 수 있을 때만 성공합니다.</span>
|
|
857
|
+
</p>
|
|
1004
858
|
</article>
|
|
1005
859
|
</div>
|
|
1006
860
|
</section>
|
|
1007
861
|
|
|
1008
|
-
<section id="adapters" data-doc-section
|
|
1009
|
-
<p class="eyebrow">
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
Adapter functions return objects or functions shaped for the target ecosystem.
|
|
1013
|
-
TypeSea does not import tRPC, Fastify, or React Hook Form at runtime.
|
|
862
|
+
<section id="adapters" data-doc-section>
|
|
863
|
+
<p class="eyebrow">
|
|
864
|
+
<span class="i18n-en">Adapters</span>
|
|
865
|
+
<span class="i18n-ko" lang="ko">어댑터</span>
|
|
1014
866
|
</p>
|
|
1015
|
-
<
|
|
1016
|
-
<
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
<td><code>toFastifyRouteSchema</code></td>
|
|
1033
|
-
<td>Lossless JSON Schema fragments for body, querystring, params, or headers.</td>
|
|
1034
|
-
</tr>
|
|
1035
|
-
<tr>
|
|
1036
|
-
<td>Fastify</td>
|
|
1037
|
-
<td><code>toFastifyValidatorCompiler</code></td>
|
|
1038
|
-
<td>Route validators dispatched by <code>httpPart</code>, producing <code>{ value }</code> or <code>{ error }</code>.</td>
|
|
1039
|
-
</tr>
|
|
1040
|
-
<tr>
|
|
1041
|
-
<td>React Hook Form</td>
|
|
1042
|
-
<td><code>toReactHookFormResolver</code></td>
|
|
1043
|
-
<td>Async resolver returning a nested field-error tree that RHF traverses by path.</td>
|
|
1044
|
-
</tr>
|
|
1045
|
-
</tbody>
|
|
1046
|
-
</table>
|
|
867
|
+
<h2>
|
|
868
|
+
<span class="i18n-en">Structural adapters without framework dependencies.</span>
|
|
869
|
+
<span class="i18n-ko" lang="ko">프레임워크 의존성 없이 제공되는 구조적 어댑터.</span>
|
|
870
|
+
</h2>
|
|
871
|
+
<div class="grid">
|
|
872
|
+
<article class="tile">
|
|
873
|
+
<h3>tRPC</h3>
|
|
874
|
+
<p><code>toTrpcParser</code> and <code>toAsyncTrpcParser</code>.</p>
|
|
875
|
+
</article>
|
|
876
|
+
<article class="tile">
|
|
877
|
+
<h3>Fastify</h3>
|
|
878
|
+
<p><code>toFastifyRouteSchema</code> and <code>toFastifyValidatorCompiler</code>.</p>
|
|
879
|
+
</article>
|
|
880
|
+
<article class="tile">
|
|
881
|
+
<h3>React Hook Form</h3>
|
|
882
|
+
<p><code>toReactHookFormResolver</code>.</p>
|
|
883
|
+
</article>
|
|
1047
884
|
</div>
|
|
1048
885
|
</section>
|
|
1049
886
|
|
|
1050
|
-
<section id="benchmarks" data-doc-section
|
|
1051
|
-
<p class="eyebrow">
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
The suite compares the runtime plan against compiled TypeSea, and both against
|
|
1055
|
-
Zod, Valibot, and Ajv over one JSON-compatible strict-object contract. Comparison
|
|
1056
|
-
packages are development-only measurement tools, and diagnostic-path
|
|
1057
|
-
(<code>check()</code> vs <code>safeParse</code>) rows keep the numbers
|
|
1058
|
-
apples-to-apples.
|
|
887
|
+
<section id="benchmarks" data-doc-section>
|
|
888
|
+
<p class="eyebrow">
|
|
889
|
+
<span class="i18n-en">Benchmarks</span>
|
|
890
|
+
<span class="i18n-ko" lang="ko">벤치마크</span>
|
|
1059
891
|
</p>
|
|
1060
|
-
<
|
|
1061
|
-
<
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
</thead>
|
|
1068
|
-
<tbody>
|
|
1069
|
-
<tr>
|
|
1070
|
-
<td>TypeSea runtime plan <code>is()</code></td>
|
|
1071
|
-
<td class="num">496,270</td>
|
|
1072
|
-
</tr>
|
|
1073
|
-
<tr>
|
|
1074
|
-
<td>TypeSea compiled <code>is()</code></td>
|
|
1075
|
-
<td class="num"><span class="best">4,237,892</span></td>
|
|
1076
|
-
</tr>
|
|
1077
|
-
<tr>
|
|
1078
|
-
<td>Zod <code>safeParse</code></td>
|
|
1079
|
-
<td class="num">1,363,792</td>
|
|
1080
|
-
</tr>
|
|
1081
|
-
<tr>
|
|
1082
|
-
<td>Valibot <code>safeParse</code></td>
|
|
1083
|
-
<td class="num">1,384,892</td>
|
|
1084
|
-
</tr>
|
|
1085
|
-
<tr>
|
|
1086
|
-
<td>Ajv compiled</td>
|
|
1087
|
-
<td class="num">4,312,174</td>
|
|
1088
|
-
</tr>
|
|
1089
|
-
</tbody>
|
|
1090
|
-
</table>
|
|
1091
|
-
</div>
|
|
1092
|
-
<div class="table-wrap">
|
|
1093
|
-
<table>
|
|
1094
|
-
<thead>
|
|
1095
|
-
<tr>
|
|
1096
|
-
<th>Invalid object path</th>
|
|
1097
|
-
<th class="num">ops/sec</th>
|
|
1098
|
-
</tr>
|
|
1099
|
-
</thead>
|
|
1100
|
-
<tbody>
|
|
1101
|
-
<tr>
|
|
1102
|
-
<td>TypeSea runtime plan <code>is()</code></td>
|
|
1103
|
-
<td class="num">3,422,416</td>
|
|
1104
|
-
</tr>
|
|
1105
|
-
<tr>
|
|
1106
|
-
<td>TypeSea compiled <code>is()</code></td>
|
|
1107
|
-
<td class="num"><span class="best">27,125,445</span></td>
|
|
1108
|
-
</tr>
|
|
1109
|
-
<tr>
|
|
1110
|
-
<td>Zod <code>safeParse</code></td>
|
|
1111
|
-
<td class="num">83,501</td>
|
|
1112
|
-
</tr>
|
|
1113
|
-
<tr>
|
|
1114
|
-
<td>Valibot <code>safeParse</code></td>
|
|
1115
|
-
<td class="num">902,616</td>
|
|
1116
|
-
</tr>
|
|
1117
|
-
<tr>
|
|
1118
|
-
<td>Ajv compiled</td>
|
|
1119
|
-
<td class="num">28,953,501</td>
|
|
1120
|
-
</tr>
|
|
1121
|
-
</tbody>
|
|
1122
|
-
</table>
|
|
1123
|
-
</div>
|
|
1124
|
-
<p class="footnote">
|
|
1125
|
-
Local release smoke, 2026-07-04 KST, single machine — regression telemetry,
|
|
1126
|
-
not throughput guarantees. The compiled path stays at Ajv-level speed while keeping
|
|
1127
|
-
descriptor-based reads, strict symbol/non-enumerable key rejection, and immutable
|
|
1128
|
-
diagnostics.
|
|
892
|
+
<h2>
|
|
893
|
+
<span class="i18n-en">Zod, Valibot, and Ajv comparisons are rendered in the README and engine notes below.</span>
|
|
894
|
+
<span class="i18n-ko" lang="ko">Zod, Valibot, Ajv와의 비교는 아래 README와 엔진 노트에 포함되어 있습니다.</span>
|
|
895
|
+
</h2>
|
|
896
|
+
<p class="lede">
|
|
897
|
+
<span class="i18n-en">Run <code>npm run bench -- bench/ecosystem.bench.ts --run</code> for the local benchmark suite.</span>
|
|
898
|
+
<span class="i18n-ko" lang="ko">로컬 벤치마크는 <code>npm run bench -- bench/ecosystem.bench.ts --run</code>으로 실행합니다.</span>
|
|
1129
899
|
</p>
|
|
1130
|
-
<
|
|
1131
|
-
<pre><code>npm run bench -- --run</code></pre>
|
|
1132
|
-
</div>
|
|
900
|
+
<img class="benchmark-image" src="assets/benchmark-headline.svg" alt="TypeSea benchmark comparison">
|
|
1133
901
|
</section>
|
|
1134
902
|
|
|
1135
|
-
<section id="release" data-doc-section
|
|
1136
|
-
<p class="eyebrow">
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
One script runs everything CI runs: source policy, docs smoke, typecheck, lint,
|
|
1140
|
-
tests, build, dist policy, public API snapshot, package contents, clean consumer
|
|
1141
|
-
install, benchmark smoke, and pack dry run. CI executes the same gate on
|
|
1142
|
-
Node 20.19, 22, and 24; releases publish with npm provenance.
|
|
903
|
+
<section id="release" data-doc-section>
|
|
904
|
+
<p class="eyebrow">
|
|
905
|
+
<span class="i18n-en">Release gate</span>
|
|
906
|
+
<span class="i18n-ko" lang="ko">릴리스 게이트</span>
|
|
1143
907
|
</p>
|
|
908
|
+
<h2>
|
|
909
|
+
<span class="i18n-en">Use the same gate locally and in CI.</span>
|
|
910
|
+
<span class="i18n-ko" lang="ko">로컬과 CI에서 같은 gate를 실행합니다.</span>
|
|
911
|
+
</h2>
|
|
1144
912
|
<div class="code-block">
|
|
1145
913
|
<pre><code>npm run release:check</code></pre>
|
|
1146
914
|
</div>
|
|
1147
|
-
<div class="callout">
|
|
1148
|
-
<span class="icon">🛡</span>
|
|
1149
|
-
<p>
|
|
1150
|
-
<strong><code>prepack</code> re-runs the full check</strong>, so even a bare
|
|
1151
|
-
<code>npm publish</code> cannot skip the gate. Package policy rejects any runtime
|
|
1152
|
-
dependency fields before the tarball is accepted.
|
|
1153
|
-
</p>
|
|
1154
|
-
</div>
|
|
1155
915
|
</section>
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
916
|
+
<section id="files" data-doc-section>
|
|
917
|
+
<p class="eyebrow">
|
|
918
|
+
<span class="i18n-en">Source files</span>
|
|
919
|
+
<span class="i18n-ko" lang="ko">원본 파일</span>
|
|
920
|
+
</p>
|
|
921
|
+
<h2>
|
|
922
|
+
<span class="i18n-en">The site below renders every maintained documentation source.</span>
|
|
923
|
+
<span class="i18n-ko" lang="ko">아래에는 현재 유지보수 중인 문서 원본 전체가 그대로 들어 있습니다.</span>
|
|
924
|
+
</h2>
|
|
925
|
+
<div class="grid">
|
|
1161
926
|
<article class="tile">
|
|
1162
|
-
<h3>
|
|
1163
|
-
<p>
|
|
927
|
+
<h3>README.md</h3>
|
|
928
|
+
<p>
|
|
929
|
+
<span class="i18n-en">Project goal, benchmark headline, quick start, API summary, edge semantics, and release workflow.</span>
|
|
930
|
+
<span class="i18n-ko" lang="ko">프로젝트 목표, 벤치마크 요약, 빠른 시작, API 개요, 경계 동작, 릴리스 흐름을 다룹니다.</span>
|
|
931
|
+
</p>
|
|
932
|
+
<code><a href="https://github.com/Feralthedogg/TypeSea">GitHub README</a></code>
|
|
933
|
+
</article>
|
|
934
|
+
<article class="tile">
|
|
935
|
+
<h3>docs/api.md</h3>
|
|
936
|
+
<p>
|
|
937
|
+
<span class="i18n-en">Guard, builder, decoder, compile, AOT, adapter, graph, JSON Schema, edge, and Result contracts.</span>
|
|
938
|
+
<span class="i18n-ko" lang="ko">가드, 빌더, 디코더, 컴파일, AOT, 어댑터, 그래프, JSON Schema, 경계 조건, Result 계약을 정리합니다.</span>
|
|
939
|
+
</p>
|
|
1164
940
|
<code><a href="https://github.com/Feralthedogg/TypeSea/blob/main/docs/api.md">docs/api.md</a></code>
|
|
1165
941
|
</article>
|
|
1166
942
|
<article class="tile">
|
|
1167
|
-
<h3>
|
|
1168
|
-
<p>
|
|
943
|
+
<h3>docs/engine-notes.md</h3>
|
|
944
|
+
<p>
|
|
945
|
+
<span class="i18n-en">Hot path rules, type-system rules, Sea-of-Nodes validation IR, compiler notes, recursion, and benchmark scope.</span>
|
|
946
|
+
<span class="i18n-ko" lang="ko">핫패스 규칙, 타입 시스템 규칙, Sea-of-Nodes 검증 IR, 컴파일러 설계, 재귀 처리, 벤치마크 범위를 설명합니다.</span>
|
|
947
|
+
</p>
|
|
1169
948
|
<code><a href="https://github.com/Feralthedogg/TypeSea/blob/main/docs/engine-notes.md">docs/engine-notes.md</a></code>
|
|
1170
949
|
</article>
|
|
1171
950
|
</div>
|
|
1172
951
|
</section>
|
|
952
|
+
<article id="readme" class="source-doc doc-content" data-doc-section>
|
|
953
|
+
<header>
|
|
954
|
+
<p class="eyebrow">
|
|
955
|
+
<span class="i18n-en">README</span>
|
|
956
|
+
<span class="i18n-ko" lang="ko">README</span>
|
|
957
|
+
</p>
|
|
958
|
+
<h2>
|
|
959
|
+
<span class="i18n-en">README</span>
|
|
960
|
+
<span class="i18n-ko" lang="ko">README</span>
|
|
961
|
+
</h2>
|
|
962
|
+
<p>
|
|
963
|
+
<span class="i18n-en">Rendered from <code>README.md</code>.</span>
|
|
964
|
+
<span class="i18n-ko" lang="ko"><code>docs/ko/readme.md</code>에서 렌더링했습니다.</span>
|
|
965
|
+
</p>
|
|
966
|
+
</header>
|
|
967
|
+
<div class="locale-en">
|
|
968
|
+
<h2 id="readme-en-typesea">TypeSea</h2>
|
|
969
|
+
<p><a href="https://github.com/Feralthedogg/TypeSea/actions/workflows/ci.yml/badge.svg"> <span class="muted-link"> <span class="muted-link"> <span class="muted-link">TypeScript</span> <span class="muted-link">Dependencies</span> <span class="muted-link">Tree-shakeable</span> <span class="muted-link">Side-effect free</span> <span class="muted-link">No dependencies</span> <span class="muted-link">Module</span> <span class="muted-link">Node</span></p>
|
|
970
|
+
<p><strong>TypeSea</strong> is a <strong>zero-runtime-dependency TypeScript runtime narrowing library</strong> built around <strong>immutable guards</strong>, optimized <strong>Sea-of-Nodes validation plans</strong>, runtime compilation, and AOT source generation.</p>
|
|
971
|
+
<h3 id="readme-en-benchmark-headline">Benchmark Headline</h3>
|
|
972
|
+
<p>Last local benchmark on 2026-07-04 KST: <code>npm run bench -- bench/ecosystem.bench.ts --run</code>, strict-object contract, operations per second on one machine.</p>
|
|
973
|
+
<p><img class="benchmark-image" src="assets/benchmark-headline.svg" alt="TypeSea benchmark comparison"></p>
|
|
974
|
+
<p>TypeSea safe compiled validators are already in Ajv's boolean hot-path class while keeping descriptor-based hostile-input semantics. Unsafe and unchecked FastMode are the bragging-rights path for trusted normalized data: direct field loads, allocation-light strict-key loops, and V8-friendly monomorphic codegen.</p>
|
|
975
|
+
<blockquote><p>Goal: not "probably valid", but <strong>provably parity-tested validation</strong> that never executes user code, never throws on expected failures, and never leaks mutable state across a public boundary.</p></blockquote>
|
|
976
|
+
<aside class="admonition important"><strong class="admonition-title">IMPORTANT</strong><p>TypeSea is designed for <strong>hostile boundary data</strong>: property reads go through descriptors so <strong>user getters never execute</strong>, <code>__proto__</code>/<code>constructor</code> keys are handled with null-prototype lookups, user regexes are cloned and <code>lastIndex</code>-reset, and cyclic inputs validate finitely. Expected failures return frozen <code>Result</code> values — <code>any</code>, <code>try</code>, and <code>catch</code> are banned from the entire codebase and enforced by policy gates.</p></aside>
|
|
977
|
+
<hr>
|
|
978
|
+
<h3 id="readme-en-why">Why</h3>
|
|
979
|
+
<p>Many validation libraries fall short when you care about:</p>
|
|
980
|
+
<ul><li><strong>untrusted input that fights back</strong> (getters with side effects, prototype</li></ul>
|
|
981
|
+
<p>pollution keys, forged schema objects, revoked proxies)</p>
|
|
982
|
+
<ul><li><strong>identical verdicts across execution strategies</strong> (runtime plan vs compiled</li></ul>
|
|
983
|
+
<p>vs AOT-generated validators)</p>
|
|
984
|
+
<ul><li><strong>diagnostics without exceptions</strong> (<code>Result</code> values instead of <code>throw</code>)</li><li><strong>immutability at every public boundary</strong></li></ul>
|
|
985
|
+
<p>TypeSea focuses on:</p>
|
|
986
|
+
<ul><li><strong>no user-code execution during validation</strong></li><li><strong>runtime plan / compiled / AOT parity, enforced by a seeded generative fuzzer</strong></li><li><strong>injection-safe code generation</strong> (side tables, never string interpolation)</li><li><strong>explicit presence semantics</strong> (<code>optional</code> vs <code>undefinedable</code>)</li></ul>
|
|
987
|
+
<hr>
|
|
988
|
+
<h3 id="readme-en-key-properties">Key Properties</h3>
|
|
989
|
+
<ul><li><strong>Zero dependencies</strong>: no runtime, peer, optional, or bundled dependencies —</li></ul>
|
|
990
|
+
<p>mechanically enforced by package policy before every release.</p>
|
|
991
|
+
<ul><li><strong>Three engines, one semantics</strong>: <code>is()</code>/<code>check()</code> execute a cached validation</li></ul>
|
|
992
|
+
<p>plan, <code>compile()</code> emits runtime predicates from optimized IR, and <code>emitAotModule()</code> emits standalone validator source. The runtime plan owns both the graph and a schema-specialized kernel, so the graph is the source of truth for generated validators without forcing ordinary <code>is()</code> through a per-node interpreter. Parity is fuzz-tested with sparse arrays, accessor properties, symbol keys, and non-enumerable extras included.</p>
|
|
993
|
+
<ul><li><strong>Frozen public surface</strong>: guards, schemas, graphs, diagnostics, and JSON</li></ul>
|
|
994
|
+
<p>Schema payloads are frozen before they cross an API boundary.</p>
|
|
995
|
+
<ul><li><strong>Lossless-only export</strong>: JSON Schema and AOT export succeed only when no</li></ul>
|
|
996
|
+
<p>semantics would be lost; runtime-only contracts return typed issues instead of silently weakening the schema.</p>
|
|
997
|
+
<aside class="admonition note"><strong class="admonition-title">NOTE</strong><p>TypeSea is <strong>ESM-only</strong>: the package ships <code>"type": "module"</code> with no CommonJS build. Node.js <code>>= 20.19</code> can also load it via <code>require(esm)</code> through the <code>default</code> export condition.</p></aside>
|
|
998
|
+
<hr>
|
|
999
|
+
<h3 id="readme-en-quick-start">Quick Start</h3>
|
|
1000
|
+
<pre><code>import { compile, t, toJsonSchema, type Infer } from "typesea";
|
|
1001
|
+
|
|
1002
|
+
const User = t.strictObject({
|
|
1003
|
+
id: t.string.uuid(),
|
|
1004
|
+
email: t.string.email(),
|
|
1005
|
+
age: t.number.int().nonnegative(),
|
|
1006
|
+
role: t.enum(["admin", "user"]),
|
|
1007
|
+
tags: t.array(t.string.min(1)).max(8)
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
type User = Infer<typeof User>;
|
|
1011
|
+
|
|
1012
|
+
// 1) Boolean narrowing — avoids diagnostic allocation on success
|
|
1013
|
+
if (User.is(input)) {
|
|
1014
|
+
input.id; // narrowed
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// 2) Immutable diagnostics — frozen Result, never throws on expected failure
|
|
1018
|
+
const checked = User.check(input);
|
|
1019
|
+
if (!checked.ok) {
|
|
1020
|
+
console.log(checked.error); // frozen issue list with paths
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// 3) Hot path — generated validator code
|
|
1024
|
+
const FastUser = compile(User, { name: "isUser" });
|
|
1025
|
+
|
|
1026
|
+
// 4) Interop — lossless-only JSON Schema export
|
|
1027
|
+
const schema = toJsonSchema(User);</code></pre>
|
|
1028
|
+
<p>Use <code>is()</code> for the allocation-light boolean path. Use <code>check()</code> when callers need the full immutable diagnostic list, or <code>checkFirst()</code> when a hot rejection path only needs one machine-readable issue. Use <code>compile()</code> or <code>emitAotModule()</code> when a stable schema is hot enough to deserve generated validator code. Compiled and AOT <code>checkFirst()</code> use a dedicated first-fault collector instead of building the full issue list and slicing it afterward.</p>
|
|
1029
|
+
<aside class="admonition caution"><strong class="admonition-title">CAUTION</strong><p><code>compile()</code> builds the validator with <code>new Function</code>, which throws under a Content-Security-Policy that forbids <code>unsafe-eval</code>. In CSP-restricted environments, generate validator source ahead of time with <code>emitAotModule()</code> instead.</p></aside>
|
|
1030
|
+
<h4 id="readme-en-unsafe-fastmode">Unsafe FastMode</h4>
|
|
1031
|
+
<pre><code>const FastButLooseUser = compile(User, {
|
|
1032
|
+
name: "isUserFast",
|
|
1033
|
+
mode: "unsafe"
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
const FastTrustedShapeUser = compile(User, {
|
|
1037
|
+
name: "isUserTrustedShape",
|
|
1038
|
+
mode: "unchecked"
|
|
1039
|
+
});</code></pre>
|
|
1040
|
+
<p><code>compile(..., { mode: "unsafe" })</code> and <code>emitAotModule(..., { mode: "unsafe" })</code> emit the V8-friendliest predicate TypeSea can generate: required object fields are read with direct bracket access, arrays and tuples use direct indexed loads, discriminants avoid descriptor reads, and strict-object extras are checked with an allocation-free <code>for...in</code> loop. This mode is for trusted, already-normalized data on extremely hot paths.</p>
|
|
1041
|
+
<p>The default is still <code>mode: "safe"</code>. Unsafe mode may execute getters, may accept prototype-backed values, and strict objects do not reject symbol or non-enumerable extras. Use it only when the caller owns the object graph or has already normalized input into plain data records. Unsafe generated predicates may also embed escaped static property keys directly in source so V8 can use ordinary property-load inline caches.</p>
|
|
1042
|
+
<p><code>mode: "unchecked"</code> goes one step further: it trusts the object shape and skips strict extra-key loops entirely. That is the fastest path for already-owned DTOs, but strict objects no longer reject any extra keys.</p>
|
|
1043
|
+
<p>In unsafe and unchecked modes, successful compiled <code>check()</code> calls return a raw <code>{ ok: true, value }</code> object instead of freezing the success result. Failed diagnostics are still frozen. Safe mode keeps the fully frozen Result contract. FastMode diagnostic collectors also use the same trusted direct-read object shape where possible, so their issue codes can be less hostile-input-specific than safe mode for missing/accessor-backed fields and sparse/accessor-backed array or record slots. Discriminant diagnostics also read tags directly.</p>
|
|
1044
|
+
<hr>
|
|
1045
|
+
<h3 id="readme-en-presence-semantics">Presence Semantics</h3>
|
|
1046
|
+
<p>Object presence is explicit — two different wrappers express two different contracts:</p>
|
|
1047
|
+
<div class="table-wrap"><table><thead><tr><th>Wrapper</th><th>Key may be absent</th><th>Value may be <code>undefined</code></th><th>Inferred type</th></tr></thead><tbody><tr><td><code>t.optional(inner)</code></td><td>yes</td><td>no</td><td><code>key?: T</code></td></tr>
|
|
1048
|
+
<tr><td><code>t.undefinedable(inner)</code></td><td>no</td><td>yes</td><td><code>key: T | undefined</code></td></tr>
|
|
1049
|
+
<tr><td><code>t.nullable(inner)</code></td><td>—</td><td>value may be <code>null</code></td><td><code>key: T | null</code></td></tr></tbody></table></div>
|
|
1050
|
+
<aside class="admonition note"><strong class="admonition-title">NOTE</strong><p>Presence survives wrapper composition: <code>t.nullable(t.optional(x))</code> still means "the key may be absent" — inference and runtime agree on this under <code>exactOptionalPropertyTypes</code>.</p></aside>
|
|
1051
|
+
<hr>
|
|
1052
|
+
<h3 id="readme-en-execution-model">Execution Model</h3>
|
|
1053
|
+
<p>TypeSea keeps the public schema tree for builder validation and diagnostics, then lowers each schema identity into a cached validation plan. The plan owns an optimized Sea-of-Nodes graph and a schema-specialized predicate kernel. <code>Guard.is()</code> uses the kernel to avoid per-node interpreter dispatch, while <code>compile()</code> and <code>emitAotModule()</code> emit predicates from the optimized graph. <code>check()</code> first asks the same plan for the verdict; failed values then replay the schema-aware diagnostic collector to produce issue paths and codes.</p>
|
|
1054
|
+
<pre><code>builder -> frozen schema -> lower -> Sea-of-Nodes IR -> optimize
|
|
1055
|
+
optimize -> ValidationPlan { graph, schema kernel }
|
|
1056
|
+
schema kernel -> Guard.is() / check() preflight
|
|
1057
|
+
graph -> compile() predicate / emitAotModule() predicate / Guard.graph()
|
|
1058
|
+
failed check() -> schema-aware diagnostic collector</code></pre>
|
|
1059
|
+
<aside class="admonition important"><strong class="admonition-title">IMPORTANT</strong><p>Generated validators keep <strong>user-controlled values out of source text</strong>: literals, regexps, object keys, keysets, and dynamic schema fallbacks live in <strong>side tables</strong> referenced by numeric index. Hostile property names cannot escape into generated code — this is pinned by dedicated injection-audit tests.</p></aside>
|
|
1060
|
+
<hr>
|
|
1061
|
+
<h3 id="readme-en-performance-snapshot">Performance Snapshot</h3>
|
|
1062
|
+
<p>Last local benchmark on 2026-07-04 KST, using <code>npm run bench -- bench/ecosystem.bench.ts --run</code> on the benchmark strict-object contract. These are operations per second on one machine, not release guarantees.</p>
|
|
1063
|
+
<div class="table-wrap"><table><thead><tr><th>Valid object path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>513,701</td></tr>
|
|
1064
|
+
<tr><td>TypeSea compiled safe <code>is()</code></td><td>4,297,306</td></tr>
|
|
1065
|
+
<tr><td>TypeSea compiled unsafe <code>is()</code></td><td>36,297,653</td></tr>
|
|
1066
|
+
<tr><td>TypeSea compiled unchecked <code>is()</code></td><td>42,581,174</td></tr>
|
|
1067
|
+
<tr><td>Zod <code>safeParse</code></td><td>1,343,756</td></tr>
|
|
1068
|
+
<tr><td>Valibot <code>safeParse</code></td><td>1,406,528</td></tr>
|
|
1069
|
+
<tr><td>Ajv compiled</td><td>4,275,389</td></tr></tbody></table></div>
|
|
1070
|
+
<div class="table-wrap"><table><thead><tr><th>Valid diagnostic path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>503,232</td></tr>
|
|
1071
|
+
<tr><td>TypeSea compiled safe <code>check()</code></td><td>3,903,929</td></tr>
|
|
1072
|
+
<tr><td>TypeSea compiled unsafe <code>check()</code></td><td>35,568,425</td></tr>
|
|
1073
|
+
<tr><td>TypeSea compiled unchecked <code>check()</code></td><td>40,084,605</td></tr>
|
|
1074
|
+
<tr><td>Zod <code>safeParse</code></td><td>1,355,014</td></tr>
|
|
1075
|
+
<tr><td>Valibot <code>safeParse</code></td><td>1,378,266</td></tr>
|
|
1076
|
+
<tr><td>Ajv compiled</td><td>4,278,587</td></tr></tbody></table></div>
|
|
1077
|
+
<div class="table-wrap"><table><thead><tr><th>Invalid object path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>3,636,369</td></tr>
|
|
1078
|
+
<tr><td>TypeSea compiled safe <code>is()</code></td><td>42,080,241</td></tr>
|
|
1079
|
+
<tr><td>TypeSea compiled unsafe <code>is()</code></td><td>49,654,076</td></tr>
|
|
1080
|
+
<tr><td>TypeSea compiled unchecked <code>is()</code></td><td>50,482,732</td></tr>
|
|
1081
|
+
<tr><td>Zod <code>safeParse</code></td><td>84,272</td></tr>
|
|
1082
|
+
<tr><td>Valibot <code>safeParse</code></td><td>878,521</td></tr>
|
|
1083
|
+
<tr><td>Ajv compiled</td><td>27,820,643</td></tr></tbody></table></div>
|
|
1084
|
+
<div class="table-wrap"><table><thead><tr><th>Invalid diagnostic path</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>420,446</td></tr>
|
|
1085
|
+
<tr><td>TypeSea compiled safe <code>check()</code></td><td>2,086,129</td></tr>
|
|
1086
|
+
<tr><td>TypeSea compiled unsafe <code>check()</code></td><td>3,077,367</td></tr>
|
|
1087
|
+
<tr><td>TypeSea compiled unchecked <code>check()</code></td><td>3,673,508</td></tr>
|
|
1088
|
+
<tr><td>Zod <code>safeParse</code></td><td>79,613</td></tr>
|
|
1089
|
+
<tr><td>Valibot <code>safeParse</code></td><td>887,991</td></tr>
|
|
1090
|
+
<tr><td>Ajv compiled</td><td>28,713,035</td></tr></tbody></table></div>
|
|
1091
|
+
<p>The safe compiled path stays close to Ajv while retaining TypeSea hostile-input semantics: descriptor-based property reads, symbol/non-enumerable strict-key rejection, presence semantics, immutable diagnostics, and TypeScript guard inference. Unsafe and unchecked compiled modes are faster because they deliberately give up parts of that hostile-input contract.</p>
|
|
1092
|
+
<hr>
|
|
1093
|
+
<h3 id="readme-en-api-reference">API Reference</h3>
|
|
1094
|
+
<p>All public entry points are exported from the package root; builders are also grouped under the <code>t</code> table.</p>
|
|
1095
|
+
<h4 id="readme-en-builders">Builders</h4>
|
|
1096
|
+
<div class="table-wrap"><table><thead><tr><th>Area</th><th>Entry points</th></tr></thead><tbody><tr><td>Scalar guards</td><td><code>t.unknown</code>, <code>t.never</code>, <code>t.string</code>, <code>t.number</code>, <code>t.date</code>, <code>t.bigint</code>, <code>t.symbol</code>, <code>t.boolean</code>, <code>t.null</code>, <code>t.undefined</code>, <code>t.void</code></td></tr>
|
|
1097
|
+
<tr><td>String checks</td><td><code>.min</code>, <code>.max</code>, <code>.length</code>, <code>.nonempty</code>, <code>.regex</code>, <code>.startsWith</code>, <code>.endsWith</code>, <code>.includes</code>, <code>.uuid</code>, <code>.email</code>, <code>.url</code>, <code>.isoDate</code>, <code>.isoDateTime</code>, <code>.ulid</code>, <code>.ipv4</code>, <code>.ipv6</code></td></tr>
|
|
1098
|
+
<tr><td>Number checks</td><td><code>.int</code>, <code>.finite</code>, <code>.safe</code>, <code>.gte</code>, <code>.lte</code>, <code>.min</code>, <code>.max</code>, <code>.gt</code>, <code>.lt</code>, <code>.multipleOf</code>, <code>.positive</code>, <code>.nonnegative</code>, <code>.negative</code>, <code>.nonpositive</code></td></tr>
|
|
1099
|
+
<tr><td>Date checks</td><td><code>.min</code>, <code>.max</code></td></tr>
|
|
1100
|
+
<tr><td>Literal and containers</td><td><code>t.literal</code>, <code>t.enum</code>, <code>t.array</code>, <code>t.tuple</code>, tuple rest, <code>t.record</code>, <code>t.map</code>, <code>t.set</code>, <code>t.json</code></td></tr>
|
|
1101
|
+
<tr><td>Array checks</td><td><code>.min</code>, <code>.max</code>, <code>.length</code>, <code>.nonempty</code></td></tr>
|
|
1102
|
+
<tr><td>Objects</td><td><code>t.object</code>, <code>t.strictObject</code>, <code>extend</code>, <code>safeExtend</code>, <code>merge</code>, <code>pick</code>, <code>omit</code>, <code>partial</code>, <code>deepPartial</code>, <code>required</code>, <code>strict</code>, <code>passthrough</code>, <code>strip</code>, <code>catchall</code></td></tr>
|
|
1103
|
+
<tr><td>Runtime object contracts</td><td><code>t.instanceOf</code>, <code>t.property</code>, <code>guard.property</code></td></tr>
|
|
1104
|
+
<tr><td>Composition</td><td><code>t.union</code>, <code>t.discriminatedUnion</code>, <code>t.intersect</code>, <code>guard.intersect</code></td></tr>
|
|
1105
|
+
<tr><td>Presence wrappers</td><td><code>t.optional</code>, <code>t.undefinedable</code>, <code>t.nullable</code>, <code>t.nullish</code></td></tr>
|
|
1106
|
+
<tr><td>Dynamic contracts</td><td><code>t.lazy</code>, <code>t.refine</code></td></tr></tbody></table></div>
|
|
1107
|
+
<h4 id="readme-en-decoders">Decoders</h4>
|
|
1108
|
+
<div class="table-wrap"><table><thead><tr><th>Area</th><th>Entry points</th></tr></thead><tbody><tr><td>Sync decoders</td><td><code>t.decoder</code>, <code>t.transform</code>, <code>t.pipe</code>, <code>t.default</code>, <code>t.defaultValue</code>, <code>t.prefault</code>, <code>t.catch</code>, <code>t.codec</code>, <code>t.coerce</code>, <code>t.string.trim()</code>, <code>t.string.toLowerCase()</code>, <code>t.string.toUpperCase()</code></td></tr>
|
|
1109
|
+
<tr><td>Async decoders</td><td><code>t.asyncDecoder</code>, <code>t.asyncRefine</code>, <code>t.asyncTransform</code>, <code>t.asyncPipe</code></td></tr></tbody></table></div>
|
|
1110
|
+
<h4 id="readme-en-execution-export">Execution & Export</h4>
|
|
1111
|
+
<div class="table-wrap"><table><thead><tr><th>Area</th><th>Entry points</th></tr></thead><tbody><tr><td>Guard methods</td><td><code>guard.is()</code>, <code>guard.check()</code>, <code>guard.checkFirst()</code>, <code>guard.graph()</code></td></tr>
|
|
1112
|
+
<tr><td>Generated validators</td><td><code>compile</code>, <code>emitAotModule</code></td></tr>
|
|
1113
|
+
<tr><td>JSON Schema</td><td><code>toJsonSchema</code></td></tr>
|
|
1114
|
+
<tr><td>Messages</td><td><code>formatIssue</code>, <code>formatIssues</code>, <code>flattenIssues</code>, <code>withMessages</code></td></tr></tbody></table></div>
|
|
1115
|
+
<h4 id="readme-en-messages-adapters">Messages & Adapters</h4>
|
|
1116
|
+
<div class="table-wrap"><table><thead><tr><th>Area</th><th>Entry points</th></tr></thead><tbody><tr><td>Messages / i18n</td><td><code>formatIssue</code>, <code>formatIssues</code>, <code>flattenIssues</code>, <code>withMessages</code>, <code>defineMessages</code></td></tr>
|
|
1117
|
+
<tr><td>tRPC</td><td><code>toTrpcParser</code>, <code>toAsyncTrpcParser</code></td></tr>
|
|
1118
|
+
<tr><td>Fastify</td><td><code>toFastifyRouteSchema</code>, <code>toFastifyValidatorCompiler</code></td></tr>
|
|
1119
|
+
<tr><td>React Hook Form</td><td><code>toReactHookFormResolver</code></td></tr></tbody></table></div>
|
|
1120
|
+
<p>Adapters accept compiled guards too. Compile once at startup, then pass the compiled guard into parser or validator-compiler adapters so framework hot paths reuse the generated predicate.</p>
|
|
1121
|
+
<pre><code>const FastUser = compile(User);
|
|
1122
|
+
const trpcParser = toTrpcParser(FastUser);
|
|
1123
|
+
const fastifyCompiler = toFastifyValidatorCompiler(FastUser);
|
|
1124
|
+
|
|
1125
|
+
// Trusted normalized data only: trades hostile-input hardening for direct reads.
|
|
1126
|
+
const UnsafeUser = compile(User, { mode: "unsafe" });
|
|
1127
|
+
const internalParser = toTrpcParser(UnsafeUser);</code></pre>
|
|
1128
|
+
<aside class="admonition tip"><strong class="admonition-title">TIP</strong><p>Match the inference alias to the source kind: <code>Infer<></code> for guards, <code>InferDecoder<></code> for decoders, <code>InferAsyncDecoder<></code> for async decoders. Applying <code>Infer<></code> to a decoder resolves to <code>never</code> — if a downstream type suddenly collapses, this is the first thing to check.</p></aside>
|
|
1129
|
+
<hr>
|
|
1130
|
+
<h3 id="readme-en-edge-semantics">Edge Semantics</h3>
|
|
1131
|
+
<p>Deliberate, documented, and pinned by tests:</p>
|
|
1132
|
+
<div class="table-wrap"><table><thead><tr><th>Input</th><th>Behavior</th></tr></thead><tbody><tr><td><code>NaN</code>, <code>Infinity</code></td><td>rejected by <code>t.number</code> (finite numbers only); <code>t.literal(NaN)</code> matches <code>NaN</code></td></tr>
|
|
1133
|
+
<tr><td><code>-0</code> vs <code>0</code></td><td>literals match via <code>Object.is</code>; diagnostics format <code>-0</code> distinctly</td></tr>
|
|
1134
|
+
<tr><td>Getter-backed properties</td><td>never executed; treated as missing/invalid data</td></tr>
|
|
1135
|
+
<tr><td><code>__proto__</code>, <code>constructor</code> keys</td><td>validated as plain own keys, no pollution</td></tr>
|
|
1136
|
+
<tr><td>Sparse array holes</td><td>read as <code>undefined</code> without executing accessors</td></tr>
|
|
1137
|
+
<tr><td>Strict object extras</td><td>rejected via <code>Reflect.ownKeys</code> — including symbol keys and non-enumerable properties</td></tr>
|
|
1138
|
+
<tr><td><code>catchall</code> extras</td><td>unknown own keys are descriptor-read and validated by the catchall schema</td></tr>
|
|
1139
|
+
<tr><td><code>strip()</code></td><td>validation-only alias for accepting extras; TypeSea does not clone stripped output</td></tr>
|
|
1140
|
+
<tr><td><code>t.date</code></td><td>accepts valid JavaScript <code>Date</code> objects; <code>.min</code> and <code>.max</code> compare epoch milliseconds without reading user-overridable Date methods</td></tr>
|
|
1141
|
+
<tr><td><code>t.map</code>, <code>t.set</code>, <code>t.instanceOf</code></td><td>runtime-only contracts; JSON Schema and AOT export reject them instead of weakening semantics</td></tr>
|
|
1142
|
+
<tr><td><code>property</code></td><td>validates own data properties only; getter-backed properties are rejected</td></tr>
|
|
1143
|
+
<tr><td>Global-flag regexes</td><td>cloned at construction; <code>lastIndex</code> reset before every test</td></tr>
|
|
1144
|
+
<tr><td>UUID</td><td>accepts RFC 9562 versions 1–8 plus the nil UUID</td></tr>
|
|
1145
|
+
<tr><td>Cyclic input values</td><td>validate finitely via (value × schema) active-pair tracking</td></tr>
|
|
1146
|
+
<tr><td>Nesting depth</td><td>capped at 256 recursive frames; deeper input fails instead of overflowing the stack</td></tr></tbody></table></div>
|
|
1147
|
+
<hr>
|
|
1148
|
+
<h3 id="readme-en-best-practices-pitfalls">Best Practices & Pitfalls</h3>
|
|
1149
|
+
<aside class="admonition warning"><strong class="admonition-title">WARNING</strong><p><strong>Recursive guards need an explicit type annotation.</strong> TypeScript cannot infer a self-referential initializer (TS7022):</p>
|
|
1150
|
+
<pre><code>interface ListNode {
|
|
1151
|
+
readonly value: string;
|
|
1152
|
+
readonly next?: ListNode;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const Node: Guard<ListNode> = t.lazy((): Guard<ListNode> =>
|
|
1156
|
+
t.object({ value: t.string, next: t.optional(Node) })
|
|
1157
|
+
);</code></pre></aside>
|
|
1158
|
+
<ul><li>**Boundary data enters as <code>unknown</code>.** Do not pre-narrow with <code>as</code> — the</li></ul>
|
|
1159
|
+
<p>builder API is typed so that narrowing happens through validation.</p>
|
|
1160
|
+
<ul><li>**Recursive contracts go through <code>t.lazy</code>.** Direct schema object cycles are</li></ul>
|
|
1161
|
+
<p>rejected at construction.</p>
|
|
1162
|
+
<ul><li><strong>Choose the engine by schema lifetime.</strong> One-off schemas: runtime plan.</li></ul>
|
|
1163
|
+
<p>Stable hot schemas: <code>compile()</code>. CSP environments or build-time generation: <code>emitAotModule()</code>.</p>
|
|
1164
|
+
<ul><li><strong>Decoders do not embed in object shapes.</strong> Compose transformations with</li></ul>
|
|
1165
|
+
<p><code>t.pipe</code> around a validated shape instead of mixing decoders into <code>t.object</code> entries.</p>
|
|
1166
|
+
<hr>
|
|
1167
|
+
<h3 id="readme-en-verification">Verification</h3>
|
|
1168
|
+
<p>Every gate that CI runs is a local npm script:</p>
|
|
1169
|
+
<pre><code>npm run check # policy, docs, typecheck, lint, tests, build, dist, API snapshot, pack
|
|
1170
|
+
npm run check:consumer # tarball install + runtime/type smoke in a temp project
|
|
1171
|
+
npm run bench -- --run # benchmark smoke
|
|
1172
|
+
npm run pack:dry # package contents dry run
|
|
1173
|
+
npm run release:check # the full pre-publish gate (everything above)</code></pre>
|
|
1174
|
+
<p><code>npm run release:check</code> runs the same gate expected before publishing: typecheck, lint, tests, build, docs smoke, dist policy, public API snapshot, package contents, consumer install, benchmark smoke, and pack dry run. CI executes it on Node 20.19, 22, and 24; releases publish with npm provenance.</p>
|
|
1175
|
+
<aside class="admonition note"><strong class="admonition-title">NOTE</strong><p>Benchmark comparison packages (Zod, Valibot, Ajv) are dev dependencies only — package policy rejects them from every runtime dependency field. The benchmark suite reports both boolean-path and diagnostic-path (<code>check()</code> vs <code>safeParse</code>) comparisons, so numbers stay apples-to-apples.</p></aside>
|
|
1176
|
+
<hr>
|
|
1177
|
+
<h3 id="readme-en-documentation">Documentation</h3>
|
|
1178
|
+
<ul><li><a href="#overview">Documentation site</a></li><li><a href="#api-reference">API reference</a></li><li><a href="#engine-notes">Engine notes</a></li></ul>
|
|
1179
|
+
<hr>
|
|
1180
|
+
<h3 id="readme-en-license">License</h3>
|
|
1181
|
+
<p>MIT License. See <a href="https://github.com/Feralthedogg/TypeSea/blob/main/LICENSE">LICENSE</a>.</p>
|
|
1182
|
+
</div>
|
|
1183
|
+
<div class="locale-ko" lang="ko">
|
|
1184
|
+
<h2 id="readme-ko-typesea">TypeSea</h2>
|
|
1185
|
+
<p><strong>TypeSea</strong>는 런타임 의존성 없이 TypeScript 값을 검증하고 타입을 좁히는 라이브러리입니다. 불변 스키마, Sea-of-Nodes에서 영향을 받은 검증 IR, 런타임 컴파일, AOT 소스 생성을 한 흐름으로 묶는 것을 목표로 합니다.</p>
|
|
1186
|
+
<h3 id="readme-ko-벤치마크-요약">벤치마크 요약</h3>
|
|
1187
|
+
<p>마지막 로컬 벤치마크는 2026-07-04 KST에 실행했습니다. 명령은 <code>npm run bench -- bench/ecosystem.bench.ts --run</code>이며, strict object 계약을 대상으로 한 단일 머신의 초당 실행 횟수입니다. 아래 수치는 회귀를 잡기 위한 로컬 측정값이지, 릴리스 성능 보증값은 아닙니다.</p>
|
|
1188
|
+
<p><img class="benchmark-image" src="assets/benchmark-headline.svg" alt="TypeSea benchmark comparison"></p>
|
|
1189
|
+
<p>TypeSea의 안전 모드 컴파일 검증기는 getter 실행 방지와 strict extra key 검사 같은 적대적 입력 방어를 유지하면서도 Ajv의 boolean hot path에 가까운 성능을 냅니다. <code>unsafe</code>와 <code>unchecked</code> FastMode는 호출자가 이미 입력을 정규화했고 객체 그래프를 신뢰할 수 있을 때 쓰는 성능 우선 경로입니다. 이 모드에서는 직접 필드 로드, 할당을 줄인 strict-key loop, V8이 inline cache를 붙이기 쉬운 코드 형태를 사용합니다.</p>
|
|
1190
|
+
<blockquote><p>목표는 "대충 유효해 보이면 통과"가 아닙니다. TypeSea의 목표는 <strong>런타임 실행, 컴파일 실행, AOT 실행이 같은 판정을 내린다는 사실을 테스트로 고정하는 검증기</strong>입니다. 사용자 코드를 실행하지 않고, 예상 가능한 실패에서 예외를 던지지 않으며, 공개 API 경계 밖으로 변경 가능한 내부 상태를 내보내지 않는 것을 기본 원칙으로 둡니다.</p></blockquote>
|
|
1191
|
+
<aside class="admonition important"><strong class="admonition-title">IMPORTANT</strong><p>TypeSea는 <strong>적대적인 경계 입력</strong>을 전제로 설계했습니다. 속성 읽기는 descriptor를 통하므로 <strong>사용자 getter를 실행하지 않습니다</strong>. <code>__proto__</code>와 <code>constructor</code> key는 null-prototype lookup으로 처리하고, 사용자 regexp는 복제한 뒤 <code>lastIndex</code>를 reset하며, 순환 입력도 유한하게 검증합니다. 예상 가능한 실패는 동결된 <code>Result</code>로 반환합니다. 불명확한 타입 탈출과 암묵적 예외 흐름에 기대지 않도록 코드베이스 전체에 정책 게이트를 둡니다.</p></aside>
|
|
1192
|
+
<hr>
|
|
1193
|
+
<h3 id="readme-ko-왜-만들었나">왜 만들었나</h3>
|
|
1194
|
+
<p>검증 라이브러리를 실제 경계 입력에 쓰다 보면 다음 조건을 동시에 만족시키기 어렵습니다.</p>
|
|
1195
|
+
<ul><li>getter 부작용, prototype pollution key, 위조된 schema object, revoked proxy처럼 검증 자체에 저항하는 입력</li><li>런타임 계획, 컴파일된 검증기, AOT로 생성한 검증기 사이의 동일한 판정</li><li><code>throw</code> 대신 <code>Result</code>로 표현되는 명시적 실패</li><li>공개 API 경계를 지날 때마다 유지되는 불변성</li></ul>
|
|
1196
|
+
<p>TypeSea는 아래 원칙에 집중합니다.</p>
|
|
1197
|
+
<ul><li>검증 중 사용자 코드 실행 금지</li><li>런타임, 컴파일, AOT 실행 경로의 판정 일치를 seeded fuzzer로 검증</li><li>코드 생성 시 사용자 입력을 소스 문자열에 직접 삽입하지 않기</li><li><code>optional</code>과 <code>undefinedable</code>을 분리하는 명시적 key presence 규칙</li></ul>
|
|
1198
|
+
<hr>
|
|
1199
|
+
<h3 id="readme-ko-핵심-속성">핵심 속성</h3>
|
|
1200
|
+
<ul><li><strong>런타임 의존성 없음</strong>: runtime, peer, optional, bundled dependency가 없습니다. 릴리스 전에 package policy가 이를 기계적으로 검증합니다.</li><li><strong>세 실행 경로, 하나의 의미</strong>: <code>is()</code>와 <code>check()</code>는 cached validation plan을 실행하고, <code>compile()</code>은 최적화된 IR에서 런타임 predicate를 생성하며, <code>emitAotModule()</code>은 standalone validator source를 만듭니다. 일반 <code>is()</code>는 per-node interpreter를 타지 않고 schema-specialized kernel을 사용합니다. sparse array, accessor property, symbol key, non-enumerable extra까지 포함해 parity fuzz test를 돌립니다.</li><li><strong>동결된 공개 표면</strong>: guard, schema, graph, diagnostic, JSON Schema payload는 공개 API 경계를 넘기 전에 freeze됩니다.</li><li><strong>손실 없는 export만 허용</strong>: JSON Schema와 AOT export는 TypeSea 계약을 의미 손실 없이 표현할 수 있을 때만 성공합니다. 런타임 전용 계약은 schema를 약화시키지 않고 typed issue를 반환합니다.</li></ul>
|
|
1201
|
+
<aside class="admonition note"><strong class="admonition-title">NOTE</strong><p>TypeSea는 <strong>ESM-only</strong> 패키지입니다. <code>"type": "module"</code>만 제공하며 CommonJS build는 없습니다. Node.js <code>>= 20.19</code>에서는 <code>default</code> export condition을 통해 <code>require(esm)</code> 로드도 가능합니다.</p></aside>
|
|
1202
|
+
<hr>
|
|
1203
|
+
<h3 id="readme-ko-빠른-시작">빠른 시작</h3>
|
|
1204
|
+
<pre><code>import { compile, t, toJsonSchema, type Infer } from "typesea";
|
|
1205
|
+
|
|
1206
|
+
const User = t.strictObject({
|
|
1207
|
+
id: t.string.uuid(),
|
|
1208
|
+
email: t.string.email(),
|
|
1209
|
+
age: t.number.int().nonnegative(),
|
|
1210
|
+
role: t.enum(["admin", "user"]),
|
|
1211
|
+
tags: t.array(t.string.min(1)).max(8)
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
type User = Infer<typeof User>;
|
|
1215
|
+
|
|
1216
|
+
// 1) Boolean narrowing: 성공 경로에서 진단 객체를 만들지 않습니다.
|
|
1217
|
+
if (User.is(input)) {
|
|
1218
|
+
input.id; // narrowed
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// 2) Immutable diagnostics: 예상 가능한 실패는 Result로 받습니다.
|
|
1222
|
+
const checked = User.check(input);
|
|
1223
|
+
if (!checked.ok) {
|
|
1224
|
+
console.log(checked.error); // path가 포함된 동결 issue 목록
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// 3) Hot path: 검증 코드를 생성합니다.
|
|
1228
|
+
const FastUser = compile(User, { name: "isUser" });
|
|
1229
|
+
|
|
1230
|
+
// 4) Interop: 의미 손실이 없을 때만 JSON Schema로 내보냅니다.
|
|
1231
|
+
const schema = toJsonSchema(User);</code></pre>
|
|
1232
|
+
<p><code>is()</code>는 할당이 적은 boolean 경로에 씁니다. 호출자가 전체 실패 이유와 path를 필요로 하면 <code>check()</code>를 씁니다. hot rejection path에서 기계가 읽을 첫 번째 실패만 필요하면 <code>checkFirst()</code>를 씁니다. 스키마가 안정적이고 호출 빈도가 높다면 <code>compile()</code> 또는 <code>emitAotModule()</code>을 씁니다. compiled/AOT <code>checkFirst()</code>는 전체 issue list를 만든 뒤 자르지 않고 전용 first-fault collector를 사용합니다.</p>
|
|
1233
|
+
<aside class="admonition caution"><strong class="admonition-title">CAUTION</strong><p><code>compile()</code>은 <code>new Function</code>으로 검증기를 생성합니다. <code>unsafe-eval</code>을 금지하는 Content-Security-Policy 환경에서는 사용할 수 없습니다. CSP 제한 환경에서는 <code>emitAotModule()</code>로 빌드 시점에 validator source를 생성하세요.</p></aside>
|
|
1234
|
+
<h4 id="readme-ko-unsafe-fastmode">Unsafe FastMode</h4>
|
|
1235
|
+
<pre><code>const FastButLooseUser = compile(User, {
|
|
1236
|
+
name: "isUserFast",
|
|
1237
|
+
mode: "unsafe"
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
const FastTrustedShapeUser = compile(User, {
|
|
1241
|
+
name: "isUserTrustedShape",
|
|
1242
|
+
mode: "unchecked"
|
|
1243
|
+
});</code></pre>
|
|
1244
|
+
<p><code>compile(..., { mode: "unsafe" })</code>와 <code>emitAotModule(..., { mode: "unsafe" })</code>는 TypeSea가 생성할 수 있는 가장 V8 친화적인 predicate를 방출합니다. required object field는 direct bracket access로 읽고, array와 tuple은 direct indexed load를 쓰며, discriminant는 descriptor read를 피합니다. strict-object extra는 allocation-free <code>for...in</code> loop로 검사합니다.</p>
|
|
1245
|
+
<p>기본값은 여전히 <code>mode: "safe"</code>입니다. unsafe mode는 getter를 실행할 수 있고, prototype-backed value를 받아들일 수 있으며, strict object에서 symbol 또는 non-enumerable extra를 거부하지 않습니다. 호출자가 객체 그래프를 소유하고 있거나 입력을 plain data record로 이미 정규화한 경우에만 사용하세요.</p>
|
|
1246
|
+
<p><code>mode: "unchecked"</code>는 한 단계 더 나아가 object shape을 신뢰하고 strict extra-key loop 자체를 건너뜁니다. 이미 소유한 DTO에서는 가장 빠른 경로지만, strict object가 더 이상 extra key를 거부하지 않습니다.</p>
|
|
1247
|
+
<p>unsafe와 unchecked mode에서 successful compiled <code>check()</code>는 frozen success result 대신 raw <code>{ ok: true, value }</code> object를 반환합니다. 실패 진단은 계속 freeze됩니다. safe mode는 success와 failure 모두 frozen <code>Result</code> 계약을 유지합니다.</p>
|
|
1248
|
+
<hr>
|
|
1249
|
+
<h3 id="readme-ko-key-presence">Key Presence</h3>
|
|
1250
|
+
<p>객체 key 존재 여부는 명시적으로 표현합니다. 서로 다른 wrapper는 서로 다른 계약을 뜻합니다.</p>
|
|
1251
|
+
<div class="table-wrap"><table><thead><tr><th>Wrapper</th><th>key 생략 허용</th><th>value <code>undefined</code> 허용</th><th>추론 타입</th></tr></thead><tbody><tr><td><code>t.optional(inner)</code></td><td>yes</td><td>no</td><td><code>key?: T</code></td></tr>
|
|
1252
|
+
<tr><td><code>t.undefinedable(inner)</code></td><td>no</td><td>yes</td><td><code>key: T | undefined</code></td></tr>
|
|
1253
|
+
<tr><td><code>t.nullable(inner)</code></td><td>-</td><td>value may be <code>null</code></td><td><code>key: T | null</code></td></tr></tbody></table></div>
|
|
1254
|
+
<aside class="admonition note"><strong class="admonition-title">NOTE</strong><p>presence는 wrapper composition을 지나도 유지됩니다. <code>t.nullable(t.optional(x))</code>는 여전히 "key가 없어도 된다"는 뜻입니다. <code>exactOptionalPropertyTypes</code> 아래에서 타입 추론과 런타임 동작은 같은 의미를 가집니다.</p></aside>
|
|
1255
|
+
<hr>
|
|
1256
|
+
<h3 id="readme-ko-실행-모델">실행 모델</h3>
|
|
1257
|
+
<p>TypeSea는 builder validation과 diagnostic을 위해 public schema tree를 유지합니다. 그 뒤 각 schema identity를 cached validation plan으로 낮춥니다. plan은 최적화된 Sea-of-Nodes graph와 schema-specialized predicate kernel을 소유합니다. <code>Guard.is()</code>는 per-node interpreter dispatch를 피하려고 kernel을 사용하고, <code>compile()</code>과 <code>emitAotModule()</code>은 optimized graph에서 predicate를 방출합니다. <code>check()</code>는 먼저 같은 plan으로 판정을 얻고, 실패한 값만 schema-aware diagnostic collector로 replay해서 issue path와 code를 만듭니다.</p>
|
|
1258
|
+
<pre><code>builder -> frozen schema -> lower -> Sea-of-Nodes IR -> optimize
|
|
1259
|
+
optimize -> ValidationPlan { graph, schema kernel }
|
|
1260
|
+
schema kernel -> Guard.is() / check() preflight
|
|
1261
|
+
graph -> compile() predicate / emitAotModule() predicate / Guard.graph()
|
|
1262
|
+
failed check() -> schema-aware diagnostic collector</code></pre>
|
|
1263
|
+
<aside class="admonition important"><strong class="admonition-title">IMPORTANT</strong><p>generated validator는 <strong>사용자가 제어하는 값을 소스 문자열에 넣지 않습니다</strong>. literal, regexp, object key, keyset, dynamic schema fallback은 numeric index로 참조되는 <strong>side table</strong>에 둡니다. 적대적인 property name이 generated code 밖으로 탈출할 수 없으며, dedicated injection-audit test가 이 속성을 고정합니다.</p></aside>
|
|
1264
|
+
<hr>
|
|
1265
|
+
<h3 id="readme-ko-성능-스냅샷">성능 스냅샷</h3>
|
|
1266
|
+
<p>마지막 로컬 벤치마크는 2026-07-04 KST에 실행했습니다. <code>npm run bench -- bench/ecosystem.bench.ts --run</code>을 사용했고, benchmark strict-object 계약을 대상으로 했습니다. 아래 값은 단일 머신의 초당 실행 횟수이며 릴리스 성능 보증값은 아닙니다.</p>
|
|
1267
|
+
<div class="table-wrap"><table><thead><tr><th>유효한 객체: boolean 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>513,701</td></tr>
|
|
1268
|
+
<tr><td>TypeSea compiled safe <code>is()</code></td><td>4,297,306</td></tr>
|
|
1269
|
+
<tr><td>TypeSea compiled unsafe <code>is()</code></td><td>36,297,653</td></tr>
|
|
1270
|
+
<tr><td>TypeSea compiled unchecked <code>is()</code></td><td>42,581,174</td></tr>
|
|
1271
|
+
<tr><td>Zod <code>safeParse</code></td><td>1,343,756</td></tr>
|
|
1272
|
+
<tr><td>Valibot <code>safeParse</code></td><td>1,406,528</td></tr>
|
|
1273
|
+
<tr><td>Ajv compiled</td><td>4,275,389</td></tr></tbody></table></div>
|
|
1274
|
+
<div class="table-wrap"><table><thead><tr><th>유효한 객체: 진단 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>503,232</td></tr>
|
|
1275
|
+
<tr><td>TypeSea compiled safe <code>check()</code></td><td>3,903,929</td></tr>
|
|
1276
|
+
<tr><td>TypeSea compiled unsafe <code>check()</code></td><td>35,568,425</td></tr>
|
|
1277
|
+
<tr><td>TypeSea compiled unchecked <code>check()</code></td><td>40,084,605</td></tr>
|
|
1278
|
+
<tr><td>Zod <code>safeParse</code></td><td>1,355,014</td></tr>
|
|
1279
|
+
<tr><td>Valibot <code>safeParse</code></td><td>1,378,266</td></tr>
|
|
1280
|
+
<tr><td>Ajv compiled</td><td>4,278,587</td></tr></tbody></table></div>
|
|
1281
|
+
<div class="table-wrap"><table><thead><tr><th>잘못된 객체: boolean 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>is()</code></td><td>3,636,369</td></tr>
|
|
1282
|
+
<tr><td>TypeSea compiled safe <code>is()</code></td><td>42,080,241</td></tr>
|
|
1283
|
+
<tr><td>TypeSea compiled unsafe <code>is()</code></td><td>49,654,076</td></tr>
|
|
1284
|
+
<tr><td>TypeSea compiled unchecked <code>is()</code></td><td>50,482,732</td></tr>
|
|
1285
|
+
<tr><td>Zod <code>safeParse</code></td><td>84,272</td></tr>
|
|
1286
|
+
<tr><td>Valibot <code>safeParse</code></td><td>878,521</td></tr>
|
|
1287
|
+
<tr><td>Ajv compiled</td><td>27,820,643</td></tr></tbody></table></div>
|
|
1288
|
+
<div class="table-wrap"><table><thead><tr><th>잘못된 객체: 진단 경로</th><th>hz</th></tr></thead><tbody><tr><td>TypeSea interpreted <code>check()</code></td><td>420,446</td></tr>
|
|
1289
|
+
<tr><td>TypeSea compiled safe <code>check()</code></td><td>2,086,129</td></tr>
|
|
1290
|
+
<tr><td>TypeSea compiled unsafe <code>check()</code></td><td>3,077,367</td></tr>
|
|
1291
|
+
<tr><td>TypeSea compiled unchecked <code>check()</code></td><td>3,673,508</td></tr>
|
|
1292
|
+
<tr><td>Zod <code>safeParse</code></td><td>79,613</td></tr>
|
|
1293
|
+
<tr><td>Valibot <code>safeParse</code></td><td>887,991</td></tr>
|
|
1294
|
+
<tr><td>Ajv compiled</td><td>28,713,035</td></tr></tbody></table></div>
|
|
1295
|
+
<p>safe compiled path는 TypeSea의 적대적 입력 방어를 유지하면서 Ajv에 가깝게 동작합니다. descriptor 기반 property read, symbol/non-enumerable strict-key rejection, key presence semantics, immutable diagnostics, TypeScript guard inference를 유지합니다. unsafe와 unchecked compiled mode는 그 방어 계약 일부를 의도적으로 포기하기 때문에 더 빠릅니다.</p>
|
|
1296
|
+
<hr>
|
|
1297
|
+
<h3 id="readme-ko-api-레퍼런스-요약">API 레퍼런스 요약</h3>
|
|
1298
|
+
<p>모든 공개 진입점은 package root에서 export됩니다. builder는 <code>t</code> table 아래에도 묶여 있습니다.</p>
|
|
1299
|
+
<h4 id="readme-ko-builders">Builders</h4>
|
|
1300
|
+
<div class="table-wrap"><table><thead><tr><th>영역</th><th>Entry points</th></tr></thead><tbody><tr><td>Scalar guard</td><td><code>t.unknown</code>, <code>t.never</code>, <code>t.string</code>, <code>t.number</code>, <code>t.date</code>, <code>t.bigint</code>, <code>t.symbol</code>, <code>t.boolean</code>, <code>t.null</code>, <code>t.undefined</code>, <code>t.void</code></td></tr>
|
|
1301
|
+
<tr><td>String check</td><td><code>.min</code>, <code>.max</code>, <code>.length</code>, <code>.nonempty</code>, <code>.regex</code>, <code>.startsWith</code>, <code>.endsWith</code>, <code>.includes</code>, <code>.uuid</code>, <code>.email</code>, <code>.url</code>, <code>.isoDate</code>, <code>.isoDateTime</code>, <code>.ulid</code>, <code>.ipv4</code>, <code>.ipv6</code></td></tr>
|
|
1302
|
+
<tr><td>Number check</td><td><code>.int</code>, <code>.finite</code>, <code>.safe</code>, <code>.gte</code>, <code>.lte</code>, <code>.min</code>, <code>.max</code>, <code>.gt</code>, <code>.lt</code>, <code>.multipleOf</code>, <code>.positive</code>, <code>.nonnegative</code>, <code>.negative</code>, <code>.nonpositive</code></td></tr>
|
|
1303
|
+
<tr><td>Date check</td><td><code>.min</code>, <code>.max</code></td></tr>
|
|
1304
|
+
<tr><td>Literal과 container</td><td><code>t.literal</code>, <code>t.enum</code>, <code>t.array</code>, <code>t.tuple</code>, tuple rest, <code>t.record</code>, <code>t.map</code>, <code>t.set</code>, <code>t.json</code></td></tr>
|
|
1305
|
+
<tr><td>Array check</td><td><code>.min</code>, <code>.max</code>, <code>.length</code>, <code>.nonempty</code></td></tr>
|
|
1306
|
+
<tr><td>Object</td><td><code>t.object</code>, <code>t.strictObject</code>, <code>extend</code>, <code>safeExtend</code>, <code>merge</code>, <code>pick</code>, <code>omit</code>, <code>partial</code>, <code>deepPartial</code>, <code>required</code>, <code>strict</code>, <code>passthrough</code>, <code>strip</code>, <code>catchall</code></td></tr>
|
|
1307
|
+
<tr><td>Runtime object contract</td><td><code>t.instanceOf</code>, <code>t.property</code>, <code>guard.property</code></td></tr>
|
|
1308
|
+
<tr><td>Composition</td><td><code>t.union</code>, <code>t.discriminatedUnion</code>, <code>t.intersect</code>, <code>guard.intersect</code></td></tr>
|
|
1309
|
+
<tr><td>Presence wrapper</td><td><code>t.optional</code>, <code>t.undefinedable</code>, <code>t.nullable</code>, <code>t.nullish</code></td></tr>
|
|
1310
|
+
<tr><td>Dynamic contract</td><td><code>t.lazy</code>, <code>t.refine</code></td></tr></tbody></table></div>
|
|
1311
|
+
<h4 id="readme-ko-decoders">Decoders</h4>
|
|
1312
|
+
<div class="table-wrap"><table><thead><tr><th>영역</th><th>Entry points</th></tr></thead><tbody><tr><td>Sync decoder</td><td><code>t.decoder</code>, <code>t.transform</code>, <code>t.pipe</code>, <code>t.default</code>, <code>t.defaultValue</code>, <code>t.prefault</code>, <code>t.catch</code>, <code>t.codec</code>, <code>t.coerce</code>, <code>t.string.trim()</code>, <code>t.string.toLowerCase()</code>, <code>t.string.toUpperCase()</code></td></tr>
|
|
1313
|
+
<tr><td>Async decoder</td><td><code>t.asyncDecoder</code>, <code>t.asyncRefine</code>, <code>t.asyncTransform</code>, <code>t.asyncPipe</code></td></tr></tbody></table></div>
|
|
1314
|
+
<h4 id="readme-ko-execution-export">Execution & Export</h4>
|
|
1315
|
+
<div class="table-wrap"><table><thead><tr><th>영역</th><th>Entry points</th></tr></thead><tbody><tr><td>Guard method</td><td><code>guard.is()</code>, <code>guard.check()</code>, <code>guard.checkFirst()</code>, <code>guard.graph()</code></td></tr>
|
|
1316
|
+
<tr><td>Generated validator</td><td><code>compile</code>, <code>emitAotModule</code></td></tr>
|
|
1317
|
+
<tr><td>JSON Schema</td><td><code>toJsonSchema</code></td></tr></tbody></table></div>
|
|
1318
|
+
<h4 id="readme-ko-messages-adapters">Messages & Adapters</h4>
|
|
1319
|
+
<div class="table-wrap"><table><thead><tr><th>영역</th><th>Entry points</th></tr></thead><tbody><tr><td>Messages / i18n</td><td><code>formatIssue</code>, <code>formatIssues</code>, <code>flattenIssues</code>, <code>withMessages</code>, <code>defineMessages</code></td></tr>
|
|
1320
|
+
<tr><td>tRPC</td><td><code>toTrpcParser</code>, <code>toAsyncTrpcParser</code></td></tr>
|
|
1321
|
+
<tr><td>Fastify</td><td><code>toFastifyRouteSchema</code>, <code>toFastifyValidatorCompiler</code></td></tr>
|
|
1322
|
+
<tr><td>React Hook Form</td><td><code>toReactHookFormResolver</code></td></tr></tbody></table></div>
|
|
1323
|
+
<p>adapter도 compiled guard를 받을 수 있습니다. startup에서 한 번 compile한 뒤 parser나 validator-compiler adapter에 넘기면 framework hot path가 generated predicate를 재사용합니다.</p>
|
|
1324
|
+
<pre><code>const FastUser = compile(User);
|
|
1325
|
+
const trpcParser = toTrpcParser(FastUser);
|
|
1326
|
+
const fastifyCompiler = toFastifyValidatorCompiler(FastUser);
|
|
1327
|
+
|
|
1328
|
+
// 신뢰된 정규화 데이터 전용: 적대적 입력 방어를 direct read 성능과 맞바꿉니다.
|
|
1329
|
+
const UnsafeUser = compile(User, { mode: "unsafe" });
|
|
1330
|
+
const internalParser = toTrpcParser(UnsafeUser);</code></pre>
|
|
1331
|
+
<aside class="admonition tip"><strong class="admonition-title">TIP</strong><p>source kind에 맞는 inference alias를 쓰세요. guard에는 <code>Infer<></code>, decoder에는 <code>InferDecoder<></code>, async decoder에는 <code>InferAsyncDecoder<></code>를 씁니다. decoder에 <code>Infer<></code>를 적용하면 <code>never</code>가 됩니다. downstream type이 갑자기 collapse되면 먼저 이 부분을 확인하세요.</p></aside>
|
|
1332
|
+
<hr>
|
|
1333
|
+
<h3 id="readme-ko-경계-동작">경계 동작</h3>
|
|
1334
|
+
<p>의도적으로 정한 동작이며 테스트로 고정되어 있습니다.</p>
|
|
1335
|
+
<div class="table-wrap"><table><thead><tr><th>입력</th><th>동작</th></tr></thead><tbody><tr><td><code>NaN</code>, <code>Infinity</code></td><td><code>t.number</code>는 거부합니다. finite number만 허용합니다. <code>t.literal(NaN)</code>은 <code>NaN</code>을 match합니다.</td></tr>
|
|
1336
|
+
<tr><td><code>-0</code> vs <code>0</code></td><td>literal은 <code>Object.is</code>로 match합니다. diagnostic은 <code>-0</code>을 구분해서 format합니다.</td></tr>
|
|
1337
|
+
<tr><td>Getter-backed properties</td><td>실행하지 않습니다. missing 또는 invalid data로 취급합니다.</td></tr>
|
|
1338
|
+
<tr><td><code>__proto__</code>, <code>constructor</code> keys</td><td>pollution 없이 plain own key로 검증합니다.</td></tr>
|
|
1339
|
+
<tr><td>Sparse array holes</td><td>accessor 실행 없이 <code>undefined</code>로 읽습니다.</td></tr>
|
|
1340
|
+
<tr><td>Strict object extras</td><td><code>Reflect.ownKeys</code>로 거부합니다. symbol key와 non-enumerable property도 포함합니다.</td></tr>
|
|
1341
|
+
<tr><td><code>catchall</code> extras</td><td>unknown own key는 descriptor로 읽고 catchall schema로 검증합니다.</td></tr>
|
|
1342
|
+
<tr><td><code>strip()</code></td><td>출력 객체를 복사하지 않는 검증 전용 alias입니다. TypeSea에서는 extra key 허용 의미가 <code>passthrough()</code>와 같습니다.</td></tr>
|
|
1343
|
+
<tr><td><code>t.date</code></td><td>유효한 JavaScript <code>Date</code> 객체만 허용합니다. <code>.min</code>과 <code>.max</code>는 사용자가 덮어쓸 수 있는 Date method를 읽지 않고 epoch millisecond로 비교합니다.</td></tr>
|
|
1344
|
+
<tr><td><code>t.map</code>, <code>t.set</code>, <code>t.instanceOf</code></td><td>runtime-only contract입니다. JSON Schema와 AOT export에서는 의미를 약화시키지 않고 명시적으로 거부합니다.</td></tr>
|
|
1345
|
+
<tr><td><code>property</code></td><td>own data property만 검증합니다. getter-backed property는 거부합니다.</td></tr>
|
|
1346
|
+
<tr><td>Global-flag regexes</td><td>construction 시 clone하고, 매 test 전에 <code>lastIndex</code>를 reset합니다.</td></tr>
|
|
1347
|
+
<tr><td>UUID</td><td>RFC 9562 version 1-8과 nil UUID를 허용합니다.</td></tr>
|
|
1348
|
+
<tr><td>Cyclic input values</td><td>value x schema active-pair tracking으로 유한하게 검증합니다.</td></tr>
|
|
1349
|
+
<tr><td>Nesting depth</td><td>recursive frame 256에서 cap을 둡니다. 더 깊은 input은 stack overflow 대신 실패합니다.</td></tr></tbody></table></div>
|
|
1350
|
+
<hr>
|
|
1351
|
+
<h3 id="readme-ko-사용-팁과-주의점">사용 팁과 주의점</h3>
|
|
1352
|
+
<aside class="admonition warning"><strong class="admonition-title">WARNING</strong><p><strong>recursive guard에는 명시적 type annotation이 필요합니다.</strong> TypeScript는 self-referential initializer를 추론하지 못합니다(TS7022).</p>
|
|
1353
|
+
<pre><code>interface ListNode {
|
|
1354
|
+
readonly value: string;
|
|
1355
|
+
readonly next?: ListNode;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const Node: Guard<ListNode> = t.lazy((): Guard<ListNode> =>
|
|
1359
|
+
t.object({ value: t.string, next: t.optional(Node) })
|
|
1360
|
+
);</code></pre></aside>
|
|
1361
|
+
<ul><li>**경계 데이터는 <code>unknown</code>으로 들어옵니다.** <code>as</code>로 미리 좁히지 마세요. builder API는 validation을 통해 narrowing이 일어나도록 typed되어 있습니다.</li><li>**recursive contract는 <code>t.lazy</code>를 통합니다.** 직접 순환하는 schema object는 construction에서 거부합니다.</li><li><strong>schema lifetime에 맞춰 engine을 고르세요.</strong> 일회성 schema는 runtime plan, 안정적인 hot schema는 <code>compile()</code>, CSP 환경이나 build-time generation은 <code>emitAotModule()</code>이 맞습니다.</li><li><strong>decoder는 object shape 안에 넣지 않습니다.</strong> decoder를 <code>t.object</code> entry와 섞지 말고, validated shape 바깥에서 <code>t.pipe</code>로 transformation을 합성하세요.</li></ul>
|
|
1362
|
+
<hr>
|
|
1363
|
+
<h3 id="readme-ko-검증">검증</h3>
|
|
1364
|
+
<p>CI가 실행하는 gate는 전부 로컬 npm script입니다.</p>
|
|
1365
|
+
<pre><code>npm run check # policy, docs, typecheck, lint, tests, build, dist, API snapshot, pack
|
|
1366
|
+
npm run check:consumer # tarball install + runtime/type smoke in a temp project
|
|
1367
|
+
npm run bench -- --run # benchmark smoke
|
|
1368
|
+
npm run pack:dry # package contents dry run
|
|
1369
|
+
npm run release:check # the full pre-publish gate</code></pre>
|
|
1370
|
+
<p><code>npm run release:check</code>는 publish 전에 기대하는 동일한 gate를 실행합니다. typecheck, lint, tests, build, docs smoke, dist policy, public API snapshot, package contents, consumer install, benchmark smoke, pack dry run을 포함합니다. CI는 Node 20.19, 22, 24에서 실행하고, release는 npm provenance와 함께 publish합니다.</p>
|
|
1371
|
+
<aside class="admonition note"><strong class="admonition-title">NOTE</strong><p>benchmark 비교 패키지인 Zod, Valibot, Ajv는 dev dependency일 뿐입니다. package policy는 이들이 runtime dependency field에 들어가는 것을 거부합니다. benchmark suite는 boolean path와 diagnostic path(<code>check()</code> vs <code>safeParse</code>)를 모두 보고하므로 비교 기준을 맞춥니다.</p></aside>
|
|
1372
|
+
<hr>
|
|
1373
|
+
<h3 id="readme-ko-문서">문서</h3>
|
|
1374
|
+
<ul><li><a href="#overview">문서 사이트</a></li><li><a href="#api-reference">API 레퍼런스</a></li><li><a href="#engine-notes">엔진 노트</a></li></ul>
|
|
1375
|
+
<hr>
|
|
1376
|
+
<h3 id="readme-ko-라이선스">라이선스</h3>
|
|
1377
|
+
<p>MIT License. 자세한 내용은 <a href="https://github.com/Feralthedogg/TypeSea/blob/main/LICENSE">LICENSE</a>를 보세요.</p>
|
|
1378
|
+
</div>
|
|
1379
|
+
</article>
|
|
1380
|
+
<article id="api-reference" class="source-doc doc-content" data-doc-section>
|
|
1381
|
+
<header>
|
|
1382
|
+
<p class="eyebrow">
|
|
1383
|
+
<span class="i18n-en">API Reference</span>
|
|
1384
|
+
<span class="i18n-ko" lang="ko">API 레퍼런스</span>
|
|
1385
|
+
</p>
|
|
1386
|
+
<h2>
|
|
1387
|
+
<span class="i18n-en">API Reference</span>
|
|
1388
|
+
<span class="i18n-ko" lang="ko">API 레퍼런스</span>
|
|
1389
|
+
</h2>
|
|
1390
|
+
<p>
|
|
1391
|
+
<span class="i18n-en">Rendered from <code>docs/api.md</code>.</span>
|
|
1392
|
+
<span class="i18n-ko" lang="ko"><code>docs/ko/api.md</code>에서 렌더링했습니다.</span>
|
|
1393
|
+
</p>
|
|
1394
|
+
</header>
|
|
1395
|
+
<div class="locale-en">
|
|
1396
|
+
<h2 id="api-reference-en-typesea-api-reference">TypeSea API Reference</h2>
|
|
1397
|
+
<p>TypeSea accepts untrusted input as <code>unknown</code> and narrows it through immutable guard values. The public API is small by design; most complexity lives behind builder validation, graph introspection, diagnostics, and export checks.</p>
|
|
1398
|
+
<h3 id="api-reference-en-import">Import</h3>
|
|
1399
|
+
<pre><code>import {
|
|
1400
|
+
compile,
|
|
1401
|
+
emitAotModule,
|
|
1402
|
+
t,
|
|
1403
|
+
toJsonSchema,
|
|
1404
|
+
type Guard,
|
|
1405
|
+
type Infer
|
|
1406
|
+
} from "typesea";</code></pre>
|
|
1407
|
+
<p>The package exposes one root entry point. Subpath imports are intentionally not part of the public API. TypeSea is ESM-only and does not publish a CommonJS condition.</p>
|
|
1408
|
+
<h3 id="api-reference-en-guard-contract">Guard Contract</h3>
|
|
1409
|
+
<pre><code>interface Guard<T> {
|
|
1410
|
+
is(value: unknown): value is T;
|
|
1411
|
+
check(value: unknown): CheckResult<T>;
|
|
1412
|
+
checkFirst(value: unknown): CheckResult<T>;
|
|
1413
|
+
assert(value: unknown): asserts value is T;
|
|
1414
|
+
graph(): Graph;
|
|
1415
|
+
}</code></pre>
|
|
1416
|
+
<div class="table-wrap"><table><thead><tr><th>Method</th><th>Use it for</th><th>Contract</th></tr></thead><tbody><tr><td><code>is</code></td><td>Hot boolean narrowing</td><td>Avoids diagnostic allocation on the success path.</td></tr>
|
|
1417
|
+
<tr><td><code>check</code></td><td>Validation with issues</td><td>Returns frozen <code>Result<T, Issue[]></code> containers.</td></tr>
|
|
1418
|
+
<tr><td><code>checkFirst</code></td><td>Hot rejection diagnostics</td><td>Returns the same frozen <code>Result</code> shape, but failure contains at most one issue. Compiled and AOT guards use a dedicated first-fault collector.</td></tr>
|
|
1419
|
+
<tr><td><code>assert</code></td><td>Throwing integration boundaries</td><td>Throws <code>TypeSeaAssertionError</code> with copied, frozen issues.</td></tr>
|
|
1420
|
+
<tr><td><code>graph</code></td><td>Runtime plan introspection</td><td>Returns the validated, optimized, frozen Sea-of-Nodes graph held by the validation plan.</td></tr></tbody></table></div>
|
|
1421
|
+
<p>Diagnostic paths contain only object keys and zero-based array or tuple indexes. Public diagnostic validators reject malformed path segments before diagnostics cross the API boundary.</p>
|
|
1422
|
+
<h3 id="api-reference-en-builder-families">Builder Families</h3>
|
|
1423
|
+
<div class="table-wrap"><table><thead><tr><th>Family</th><th>Builders</th></tr></thead><tbody><tr><td>Scalars</td><td><code>t.unknown</code>, <code>t.never</code>, <code>t.string</code>, <code>t.number</code>, <code>t.date</code>, <code>t.bigint</code>, <code>t.symbol</code>, <code>t.boolean</code>, <code>t.null</code>, <code>t.undefined</code>, <code>t.void</code></td></tr>
|
|
1424
|
+
<tr><td>String checks</td><td><code>.min</code>, <code>.max</code>, <code>.length</code>, <code>.nonempty</code>, <code>.regex</code>, <code>.startsWith</code>, <code>.endsWith</code>, <code>.includes</code>, <code>.uuid</code>, <code>.email</code>, <code>.url</code>, <code>.isoDate</code>, <code>.isoDateTime</code>, <code>.ulid</code>, <code>.ipv4</code>, <code>.ipv6</code></td></tr>
|
|
1425
|
+
<tr><td>Number checks</td><td><code>.int</code>, <code>.finite</code>, <code>.safe</code>, <code>.gte</code>, <code>.lte</code>, <code>.min</code>, <code>.max</code>, <code>.gt</code>, <code>.lt</code>, <code>.multipleOf</code>, <code>.positive</code>, <code>.nonnegative</code>, <code>.negative</code>, <code>.nonpositive</code></td></tr>
|
|
1426
|
+
<tr><td>Date checks</td><td><code>.min</code>, <code>.max</code></td></tr>
|
|
1427
|
+
<tr><td>Literals and containers</td><td><code>t.literal(value)</code>, <code>t.enum(values)</code>, <code>t.array(item)</code>, <code>t.tuple([a, b])</code>, <code>t.tuple([head], rest)</code>, <code>t.record(value)</code>, <code>t.map(key, value)</code>, <code>t.set(item)</code>, <code>t.json()</code></td></tr>
|
|
1428
|
+
<tr><td>Array checks</td><td><code>.min</code>, <code>.max</code>, <code>.length</code>, <code>.nonempty</code></td></tr>
|
|
1429
|
+
<tr><td>Objects</td><td><code>t.object(shape)</code>, <code>t.strictObject(shape)</code></td></tr>
|
|
1430
|
+
<tr><td>Object transforms</td><td><code>t.extend</code>, <code>t.safeExtend</code>, <code>t.merge</code>, <code>t.pick</code>, <code>t.omit</code>, <code>t.partial</code>, <code>t.deepPartial</code>, <code>t.required</code>, <code>t.strict</code>, <code>t.passthrough</code>, <code>t.strip</code>, <code>t.catchall</code>, and matching object guard methods</td></tr>
|
|
1431
|
+
<tr><td>Runtime object contracts</td><td><code>t.instanceOf(Ctor)</code>, <code>t.property(base, key, value)</code>, <code>guard.property(key, value)</code></td></tr>
|
|
1432
|
+
<tr><td>Composition</td><td><code>t.union</code>, <code>t.discriminatedUnion</code>, <code>t.intersect</code>, <code>guard.intersect</code></td></tr>
|
|
1433
|
+
<tr><td>Presence</td><td><code>t.optional</code>, <code>t.undefinedable</code>, <code>t.nullable</code>, <code>t.nullish</code></td></tr>
|
|
1434
|
+
<tr><td>Dynamic guards</td><td><code>t.lazy</code>, <code>t.refine</code></td></tr>
|
|
1435
|
+
<tr><td>Decoders</td><td><code>t.decoder</code>, <code>t.transform</code>, <code>t.pipe</code>, <code>t.default</code>, <code>t.defaultValue</code>, <code>t.prefault</code>, <code>t.catch</code>, <code>t.codec</code>, <code>t.coerce</code>, <code>t.string.trim()</code>, <code>t.string.toLowerCase()</code>, <code>t.string.toUpperCase()</code></td></tr>
|
|
1436
|
+
<tr><td>Async decoders</td><td><code>t.asyncDecoder</code>, <code>t.asyncRefine</code>, <code>t.asyncTransform</code>, <code>t.asyncPipe</code></td></tr></tbody></table></div>
|
|
1437
|
+
<p>Builder functions validate inputs before a schema can enter the validation plan, compiler, AOT emitter, diagnostic collector, or JSON Schema exporter. Forged guard-like values, invalid schema tags, invalid predicates, invalid bounds, malformed regexps, and invalid discriminated union case sets are rejected during construction.</p>
|
|
1438
|
+
<p>Accepted schemas are frozen before storage. Public schema collection fields use frozen arrays and frozen key lookup records instead of mutable collection objects.</p>
|
|
1439
|
+
<h3 id="api-reference-en-object-presence">Object Presence</h3>
|
|
1440
|
+
<p>TypeSea separates key presence from value domain.</p>
|
|
1441
|
+
<pre><code>const Shape = t.object({
|
|
1442
|
+
name: t.optional(t.string),
|
|
1443
|
+
nickname: t.undefinedable(t.string)
|
|
1444
|
+
});</code></pre>
|
|
1445
|
+
<ul><li><code>name</code> may be absent. If <code>name</code> exists, its value must be a string.</li><li><code>nickname</code> must be present. Its value may be a string or <code>undefined</code>.</li><li><code>t.nullable(inner)</code> adds <code>null</code> to the value domain.</li><li><code>t.nullish(inner)</code> combines nullable value semantics with optional object-key</li></ul>
|
|
1446
|
+
<p>presence.</p>
|
|
1447
|
+
<ul><li>Presence-preserving wrappers keep optional-key semantics through <code>nullable</code>,</li></ul>
|
|
1448
|
+
<p><code>undefinedable</code>, <code>brand</code>, and <code>refine</code>.</p>
|
|
1449
|
+
<p>Object combinators preserve object mode. Strict object guards remain strict after <code>extend</code>, <code>pick</code>, <code>omit</code>, or <code>partial</code>; passthrough object guards keep allowing unknown keys.</p>
|
|
1450
|
+
<p><code>catchall(schema)</code> validates every undeclared own key with <code>schema</code>. <code>strip()</code> is validation-only in TypeSea: guards return the original value, so it has the same validation behavior as <code>passthrough()</code>. <code>pick</code> and <code>omit</code> accept either key arrays or Zod-style <code>{ key: true }</code> masks. <code>deepPartial()</code> recursively partializes pure object, array, tuple, tuple rest, record, map, set, property, union, intersection, nullable, undefinedable, optional, and brand schemas. Lazy and refinement schemas are semantic barriers.</p>
|
|
1451
|
+
<p><code>property</code> validates only own data descriptors. It is useful for class instances with stable fields; prototype getters and accessor-backed properties are rejected instead of executed.</p>
|
|
1452
|
+
<h3 id="api-reference-en-composition">Composition</h3>
|
|
1453
|
+
<p><code>t.union(a, b)</code> accepts a value that satisfies at least one branch.</p>
|
|
1454
|
+
<p><code>t.discriminatedUnion("kind", cases)</code> requires string case keys. Each case must be a statically inspectable object case whose dispatch key is a required string literal matching the case name.</p>
|
|
1455
|
+
<p><code>t.intersect(a, b)</code> and <code>guard.intersect(other)</code> require the same input value to satisfy both guards. <code>check()</code> collects diagnostics from both sides.</p>
|
|
1456
|
+
<h3 id="api-reference-en-recursion">Recursion</h3>
|
|
1457
|
+
<p>Recursive contracts must use <code>t.lazy</code>.</p>
|
|
1458
|
+
<pre><code>interface ListNode {
|
|
1459
|
+
readonly value: string;
|
|
1460
|
+
readonly next?: ListNode;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const Node: Guard<ListNode> = t.lazy((): Guard<ListNode> =>
|
|
1464
|
+
t.object({
|
|
1465
|
+
value: t.string,
|
|
1466
|
+
next: t.optional(Node)
|
|
1467
|
+
})
|
|
1468
|
+
);</code></pre>
|
|
1469
|
+
<p>Direct cyclic schema objects are rejected at builder boundaries. Lazy guards resolve once per guard instance and keep recursive schema identity stable. A lazy chain must eventually resolve to a concrete non-lazy schema.</p>
|
|
1470
|
+
<h3 id="api-reference-en-decoder-pipelines">Decoder Pipelines</h3>
|
|
1471
|
+
<pre><code>const Count = t.pipe(t.coerce.number(), t.number.int().gte(0));
|
|
1472
|
+
const result = Count.decode("42");
|
|
1473
|
+
|
|
1474
|
+
const Name = t.default(t.string.min(1), "anonymous");
|
|
1475
|
+
const NumberText = t.codec(
|
|
1476
|
+
t.string.regex(/^\d+$/u, "digits"),
|
|
1477
|
+
t.number.int().nonnegative(),
|
|
1478
|
+
{
|
|
1479
|
+
decode: (value) => Number(value),
|
|
1480
|
+
encode: (value) => String(value)
|
|
1481
|
+
}
|
|
1482
|
+
);</code></pre>
|
|
1483
|
+
<p>Decoders are for output-producing operations. They return <code>Result</code> from <code>decode()</code> and do not expose <code>is()</code> predicates, because the decoded output may not be the same runtime value as the input.</p>
|
|
1484
|
+
<ul><li><code>t.transform(source, mapper)</code> decodes <code>source</code>, then maps the decoded value.</li><li><code>t.pipe(source, next)</code> feeds a successful decoded value into the next guard or</li></ul>
|
|
1485
|
+
<p>decoder.</p>
|
|
1486
|
+
<ul><li><code>t.default(source, value)</code> returns a fallback output for <code>undefined</code> input.</li><li><code>t.prefault(source, value)</code> feeds a fallback input through the source.</li><li><code>t.codec(input, output, mapping)</code> validates both sides of a bidirectional</li></ul>
|
|
1487
|
+
<p>decode/encode pair.</p>
|
|
1488
|
+
<ul><li><code>t.coerce.string</code>, <code>t.coerce.number</code>, and <code>t.coerce.boolean</code> provide explicit</li></ul>
|
|
1489
|
+
<p>primitive coercion.</p>
|
|
1490
|
+
<ul><li><code>t.string.trim()</code>, <code>t.string.toLowerCase()</code>, and <code>t.string.toUpperCase()</code></li></ul>
|
|
1491
|
+
<p>are decoder helpers. They validate the string first, then return transformed output from <code>decode()</code>.</p>
|
|
1492
|
+
<ul><li><code>t.asyncRefine</code>, <code>t.asyncTransform</code>, and <code>t.asyncPipe</code> return</li></ul>
|
|
1493
|
+
<p><code>Promise<Result<T, Issue[]>></code> from <code>decodeAsync()</code>.</p>
|
|
1494
|
+
<p>Expected async validation failures still return <code>Result</code> values.</p>
|
|
1495
|
+
<h3 id="api-reference-en-messages">Messages</h3>
|
|
1496
|
+
<pre><code>const checked = withMessages(User.check(input), {
|
|
1497
|
+
locale: "ko",
|
|
1498
|
+
catalog: defineMessages({
|
|
1499
|
+
expected_string: "{path}: 문자열 필요"
|
|
1500
|
+
})
|
|
1501
|
+
});</code></pre>
|
|
1502
|
+
<p><code>formatIssue</code>, <code>formatIssues</code>, <code>flattenIssues</code>, and <code>withMessages</code> render diagnostics after validation has finished. This keeps <code>is()</code> and ordinary <code>check()</code> paths free from message allocation.</p>
|
|
1503
|
+
<p>Built-in locales are <code>en</code> and <code>ko</code>. Custom catalogs can use string templates with <code>{path}</code>, <code>{code}</code>, <code>{expected}</code>, and <code>{actual}</code>, or formatter callbacks. <code>withMessages(result, options)</code> preserves successful results and returns a new failed <code>Result</code> with copied, frozen issues whose <code>message</code> fields are populated. <code>flattenIssues(issues, options)</code> groups rendered messages into <code>formErrors</code> and top-level <code>fieldErrors</code> buckets.</p>
|
|
1504
|
+
<h3 id="api-reference-en-runtime-compile">Runtime Compile</h3>
|
|
1505
|
+
<pre><code>const FastUser = compile(User, { name: "isUser" });
|
|
1506
|
+
|
|
1507
|
+
FastUser.is(input);
|
|
1508
|
+
FastUser.check(input);</code></pre>
|
|
1509
|
+
<p><code>compile</code> emits generated predicate functions from the optimized Sea-of-Nodes validation graph plus diagnostics collectors for failed values. Static scalar, object, array, record, union, and strict-key nodes lower to straight-line JavaScript or indexed loops where possible. Dynamic schema edges such as <code>lazy</code> and <code>refine</code> keep semantics by using the same IR-backed runtime fallback as ordinary guards.</p>
|
|
1510
|
+
<p>The optional <code>name</code> is a debugging and profiling hint. TypeSea normalizes it into a strict-mode-safe JavaScript function name, prefixes reserved names, and caps generated name length. Direct compiled guard construction validates the predicate, collector, and source arguments. Collector diagnostics are validated, copied, and frozen before <code>check()</code> returns them.</p>
|
|
1511
|
+
<p>Generated source never interpolates user-controlled values directly. Literals, regexps, property keys, keysets, and dynamic schema fallbacks are captured in side tables and referenced by numeric index.</p>
|
|
1512
|
+
<h4 id="api-reference-en-unsafe-compile-mode">Unsafe Compile Mode</h4>
|
|
1513
|
+
<pre><code>const FastButLooseUser = compile(User, {
|
|
1514
|
+
name: "isUserFast",
|
|
1515
|
+
mode: "unsafe"
|
|
1516
|
+
});</code></pre>
|
|
1517
|
+
<p><code>CompileOptions["mode"]</code> and <code>AotCompileOptions["mode"]</code> are <code>"safe" | "unsafe" | "unchecked" | undefined</code>; omitted options default to <code>"safe"</code>. Safe mode keeps TypeSea's hostile-input contract: descriptor-based property reads, no getter execution, and strict-object rejection for symbol and non-enumerable extras.</p>
|
|
1518
|
+
<p>Unsafe mode is an explicit performance escape hatch for trusted, normalized plain data:</p>
|
|
1519
|
+
<ul><li>Required object fields read with <code>value[key]</code> when the field schema rejects</li></ul>
|
|
1520
|
+
<p><code>undefined</code>.</p>
|
|
1521
|
+
<ul><li>Discriminant dispatch reads the tag with direct bracket access.</li><li>Arrays and tuples read items with direct indexed loads.</li><li>Strict-object extra-key rejection uses an allocation-free own-enumerable</li></ul>
|
|
1522
|
+
<p><code>for...in</code> loop.</p>
|
|
1523
|
+
<p>This may execute getters, may accept prototype-backed values, and does not reject symbol or non-enumerable extras on strict objects. Because compiled <code>check()</code> first trusts the generated predicate verdict, an unsafe predicate that returns <code>true</code> also returns a successful <code>check()</code> result. Use unsafe mode only after the input has crossed a trusted normalization boundary.</p>
|
|
1524
|
+
<p>Unsafe mode may embed escaped static property keys directly into generated predicate source so V8 can attach ordinary property-load inline caches. Safe mode keeps property keys in side tables.</p>
|
|
1525
|
+
<p>Unchecked mode uses the unsafe direct-read shape and also skips strict-object extra-key loops. It is only for input whose object shape has already been trusted or normalized; strict objects no longer reject extra keys in this mode. Unsafe and unchecked compiled <code>check()</code> calls also return raw successful Result objects without <code>Object.freeze()</code>. Failure diagnostics remain frozen. Safe mode keeps frozen success and failure Result objects. FastMode diagnostic collectors use direct field reads and FastMode strict-key rules for object diagnostics where possible, so missing/accessor issue codes are not guaranteed to match safe mode. Array and tuple diagnostics also use direct indexed reads in fast modes, so sparse slots are diagnosed from the loaded <code>undefined</code> value. Record diagnostics use direct <code>record[key]</code> reads; unchecked mode also visits inherited enumerable keys. Discriminant diagnostics read the tag directly and compare string cases with <code>===</code>.</p>
|
|
1526
|
+
<h3 id="api-reference-en-aot-emit">AOT Emit</h3>
|
|
1527
|
+
<pre><code>const emitted = emitAotModule(User, { name: "aotUser" });
|
|
1528
|
+
const unsafeEmitted = emitAotModule(User, {
|
|
1529
|
+
name: "aotUserFast",
|
|
1530
|
+
mode: "unsafe"
|
|
1531
|
+
});
|
|
1532
|
+
const uncheckedEmitted = emitAotModule(User, {
|
|
1533
|
+
name: "aotUserTrustedShape",
|
|
1534
|
+
mode: "unchecked"
|
|
1535
|
+
});</code></pre>
|
|
1536
|
+
<p><code>emitAotModule</code> returns <code>Result<AotModule, AotIssue[]></code>. A successful result contains standalone ESM validator source plus declaration source. The generated module exports <code>is</code>, <code>check</code>, <code>assert</code>, and a default frozen guard-like object, without requiring dynamic source compilation at module load time.</p>
|
|
1537
|
+
<p>AOT generation is lossless-only. Schemas that require runtime callbacks or identity that cannot be serialized return explicit AOT issues.</p>
|
|
1538
|
+
<h3 id="api-reference-en-ecosystem-adapters">Ecosystem Adapters</h3>
|
|
1539
|
+
<pre><code>const parser = toTrpcParser(User);
|
|
1540
|
+
const routeSchema = toFastifyRouteSchema(User);
|
|
1541
|
+
const validatorCompiler = toFastifyValidatorCompiler(User);
|
|
1542
|
+
const resolver = toReactHookFormResolver(User);</code></pre>
|
|
1543
|
+
<p>Adapters are structural and zero-dependency. TypeSea does not import tRPC, Fastify, or React Hook Form.</p>
|
|
1544
|
+
<p>Compiled guards can be passed to the same adapters. This is the preferred shape for hot request paths: compile once during startup, then let the adapter reuse the generated predicate.</p>
|
|
1545
|
+
<pre><code>const FastUser = compile(User);
|
|
1546
|
+
const fastParser = toTrpcParser(FastUser);
|
|
1547
|
+
const fastValidatorCompiler = toFastifyValidatorCompiler(FastUser);</code></pre>
|
|
1548
|
+
<p>Use the default compiled mode at public input boundaries. For trusted, already-normalized internal data, the faster modes can be wired through adapters the same way.</p>
|
|
1549
|
+
<pre><code>const UnsafeUser = compile(User, { mode: "unsafe" });
|
|
1550
|
+
const internalParser = toTrpcParser(UnsafeUser);
|
|
1551
|
+
|
|
1552
|
+
const TrustedShapeUser = compile(User, { mode: "unchecked" });
|
|
1553
|
+
const internalValidatorCompiler = toFastifyValidatorCompiler(TrustedShapeUser);</code></pre>
|
|
1554
|
+
<div class="table-wrap"><table><thead><tr><th>Adapter</th><th>Export</th><th>Behavior</th></tr></thead><tbody><tr><td>tRPC</td><td><code>toTrpcParser</code>, <code>toAsyncTrpcParser</code></td><td>Return parser objects that emit decoded values or throw <code>TypeSeaAssertionError</code>.</td></tr>
|
|
1555
|
+
<tr><td>Fastify route schema</td><td><code>toFastifyRouteSchema</code></td><td>Converts guards to JSON Schema route fragments.</td></tr>
|
|
1556
|
+
<tr><td>Fastify validator compiler</td><td><code>toFastifyValidatorCompiler</code></td><td>Returns compiler-shaped validators that produce <code>{ value }</code> or <code>{ error }</code>.</td></tr>
|
|
1557
|
+
<tr><td>React Hook Form</td><td><code>toReactHookFormResolver</code></td><td>Returns an async resolver with TypeSea messages mapped to field errors.</td></tr></tbody></table></div>
|
|
1558
|
+
<h3 id="api-reference-en-graph-and-ir">Graph and IR</h3>
|
|
1559
|
+
<pre><code>const graph = User.graph();
|
|
1560
|
+
const optimized = optimizeGraph(graph);</code></pre>
|
|
1561
|
+
<p><code>Guard.graph()</code> returns the optimized Sea-of-Nodes validation graph held by the runtime validation plan. The same plan also owns the specialized predicate kernel used by <code>is()</code>. The graph is the source for <code>compile()</code> and <code>emitAotModule()</code>, while the kernel keeps ordinary guard execution out of a generic per-node interpreter. Public graph values are validated, dependency-checked, dense, and frozen.</p>
|
|
1562
|
+
<p><code>optimizeGraph(graph)</code> validates direct graph inputs before optimizing them. Regex graph nodes accept only plain <code>RegExp</code> values and store non-extensible regexps, cloning extensible inputs before the graph is frozen.</p>
|
|
1563
|
+
<p><code>SchemaCheck</code> records dynamic runtime schema logic such as <code>lazy</code> or <code>refine</code>. It keeps the IR truthful instead of pretending a callback-backed edge is a static primitive.</p>
|
|
1564
|
+
<h3 id="api-reference-en-json-schema">JSON Schema</h3>
|
|
1565
|
+
<pre><code>const result = toJsonSchema(User);</code></pre>
|
|
1566
|
+
<p><code>toJsonSchema</code> returns <code>Result<JsonSchema, JsonSchemaExportIssue[]></code>. It succeeds only when TypeSea can represent the contract over JSON-compatible input values without semantic loss.</p>
|
|
1567
|
+
<p>Runtime-only concepts return explicit export issues:</p>
|
|
1568
|
+
<ul><li><code>undefined</code></li><li><code>bigint</code></li><li><code>symbol</code></li><li>JavaScript <code>Date</code>, <code>Map</code>, <code>Set</code>, <code>instanceOf</code>, and <code>property</code> contracts</li><li><code>lazy</code></li><li><code>refine</code></li><li>decoder transforms</li><li>async validation</li><li>regexps with flags</li><li>numeric literals that JSON cannot preserve, such as <code>NaN</code>, <code>Infinity</code>, and</li></ul>
|
|
1569
|
+
<p><code>-0</code></p>
|
|
1570
|
+
<p><code>schemaToJsonSchema(schema)</code> is the direct schema API. It validates the supplied schema and freezes it before export. JSON Schema options are also validated; <code>schemaId</code>, when present, must be a string.</p>
|
|
1571
|
+
<p>Object <code>properties</code> maps are emitted as null-prototype records so special keys such as <code>__proto__</code>, <code>constructor</code>, and <code>hasOwnProperty</code> remain ordinary own schema properties.</p>
|
|
1572
|
+
<h3 id="api-reference-en-edge-semantics">Edge Semantics</h3>
|
|
1573
|
+
<ul><li>Literal guards use <code>Object.is</code>, so <code>t.literal(Number.NaN)</code> matches <code>NaN</code> and</li></ul>
|
|
1574
|
+
<p><code>t.literal(-0)</code> does not match <code>0</code>.</p>
|
|
1575
|
+
<ul><li><code>t.number</code> accepts only finite JavaScript numbers. <code>NaN</code>, <code>Infinity</code>, and</li></ul>
|
|
1576
|
+
<p><code>-Infinity</code> are rejected before configured numeric predicates run.</p>
|
|
1577
|
+
<ul><li>String length bounds must be non-negative integers.</li><li>Numeric comparison bounds must be finite.</li><li>Predicate callbacks must return strict <code>true</code>; truthy non-boolean values do</li></ul>
|
|
1578
|
+
<p>not pass validation.</p>
|
|
1579
|
+
<ul><li><code>RegExp</code> checks reset <code>lastIndex</code> before each test, so global and sticky</li></ul>
|
|
1580
|
+
<p>regexps do not leak state across validations.</p>
|
|
1581
|
+
<ul><li>String regex builders and direct string regex schemas accept only plain</li></ul>
|
|
1582
|
+
<p><code>RegExp</code> instances. Accepted regex checks are cloned before storage.</p>
|
|
1583
|
+
<h3 id="api-reference-en-result-contract">Result Contract</h3>
|
|
1584
|
+
<pre><code>type Result<T, E> =
|
|
1585
|
+
| { ok: true; value: T }
|
|
1586
|
+
| { ok: false; error: E };</code></pre>
|
|
1587
|
+
<p>Expected validation failures use <code>Result</code>. Result containers are frozen at runtime. Successful values are not recursively frozen because they are caller-owned data.</p>
|
|
1588
|
+
</div>
|
|
1589
|
+
<div class="locale-ko" lang="ko">
|
|
1590
|
+
<h2 id="api-reference-ko-typesea-api-레퍼런스">TypeSea API 레퍼런스</h2>
|
|
1591
|
+
<p>TypeSea는 신뢰할 수 없는 값을 <code>unknown</code>으로 받고, 불변 guard를 통해 타입을 좁힙니다. 공개 API는 작게 유지하고, 복잡한 검증 로직은 builder validation, graph introspection, diagnostics, export check 내부에 둡니다.</p>
|
|
1592
|
+
<h3 id="api-reference-ko-가져오기">가져오기</h3>
|
|
1593
|
+
<pre><code>import {
|
|
1594
|
+
compile,
|
|
1595
|
+
emitAotModule,
|
|
1596
|
+
t,
|
|
1597
|
+
toJsonSchema,
|
|
1598
|
+
type Guard,
|
|
1599
|
+
type Infer
|
|
1600
|
+
} from "typesea";</code></pre>
|
|
1601
|
+
<p>패키지는 root entry point 하나만 노출합니다. subpath import는 공개 API가 아닙니다. TypeSea는 ESM-only이며 CommonJS condition을 publish하지 않습니다.</p>
|
|
1602
|
+
<h3 id="api-reference-ko-guard-계약">Guard 계약</h3>
|
|
1603
|
+
<pre><code>interface Guard<T> {
|
|
1604
|
+
is(value: unknown): value is T;
|
|
1605
|
+
check(value: unknown): CheckResult<T>;
|
|
1606
|
+
checkFirst(value: unknown): CheckResult<T>;
|
|
1607
|
+
assert(value: unknown): asserts value is T;
|
|
1608
|
+
graph(): Graph;
|
|
1609
|
+
}</code></pre>
|
|
1610
|
+
<div class="table-wrap"><table><thead><tr><th>메서드</th><th>용도</th><th>계약</th></tr></thead><tbody><tr><td><code>is</code></td><td>빠른 boolean narrowing</td><td>성공 경로에서 진단 객체를 만들지 않습니다.</td></tr>
|
|
1611
|
+
<tr><td><code>check</code></td><td>실패 이유가 필요한 검증</td><td>동결된 <code>Result<T, Issue[]></code> container를 반환합니다.</td></tr>
|
|
1612
|
+
<tr><td><code>checkFirst</code></td><td>hot path의 단일 실패 진단</td><td>같은 <code>Result</code> 형태를 반환하되 실패 시 issue를 최대 하나만 담습니다. compiled/AOT guard는 전용 first-fault collector를 사용합니다.</td></tr>
|
|
1613
|
+
<tr><td><code>assert</code></td><td>예외가 필요한 연동 지점</td><td>복사되고 동결된 issue를 담은 <code>TypeSeaAssertionError</code>를 던집니다.</td></tr>
|
|
1614
|
+
<tr><td><code>graph</code></td><td>검증 계획 introspection</td><td>validation plan이 보유한 validated, optimized, frozen Sea-of-Nodes graph를 반환합니다.</td></tr></tbody></table></div>
|
|
1615
|
+
<p>diagnostic path에는 object key와 0부터 시작하는 array 또는 tuple index만 들어갑니다. 공개 diagnostic validator는 잘못된 path segment를 거부한 뒤 diagnostic을 API 밖으로 내보냅니다.</p>
|
|
1616
|
+
<h3 id="api-reference-ko-builder-계열">Builder 계열</h3>
|
|
1617
|
+
<div class="table-wrap"><table><thead><tr><th>계열</th><th>Builder</th></tr></thead><tbody><tr><td>Scalar</td><td><code>t.unknown</code>, <code>t.never</code>, <code>t.string</code>, <code>t.number</code>, <code>t.date</code>, <code>t.bigint</code>, <code>t.symbol</code>, <code>t.boolean</code>, <code>t.null</code>, <code>t.undefined</code>, <code>t.void</code></td></tr>
|
|
1618
|
+
<tr><td>String check</td><td><code>.min</code>, <code>.max</code>, <code>.length</code>, <code>.nonempty</code>, <code>.regex</code>, <code>.startsWith</code>, <code>.endsWith</code>, <code>.includes</code>, <code>.uuid</code>, <code>.email</code>, <code>.url</code>, <code>.isoDate</code>, <code>.isoDateTime</code>, <code>.ulid</code>, <code>.ipv4</code>, <code>.ipv6</code></td></tr>
|
|
1619
|
+
<tr><td>Number check</td><td><code>.int</code>, <code>.finite</code>, <code>.safe</code>, <code>.gte</code>, <code>.lte</code>, <code>.min</code>, <code>.max</code>, <code>.gt</code>, <code>.lt</code>, <code>.multipleOf</code>, <code>.positive</code>, <code>.nonnegative</code>, <code>.negative</code>, <code>.nonpositive</code></td></tr>
|
|
1620
|
+
<tr><td>Date check</td><td><code>.min</code>, <code>.max</code></td></tr>
|
|
1621
|
+
<tr><td>Literal과 container</td><td><code>t.literal(value)</code>, <code>t.enum(values)</code>, <code>t.array(item)</code>, <code>t.tuple([a, b])</code>, <code>t.tuple([head], rest)</code>, <code>t.record(value)</code>, <code>t.map(key, value)</code>, <code>t.set(item)</code>, <code>t.json()</code></td></tr>
|
|
1622
|
+
<tr><td>Array check</td><td><code>.min</code>, <code>.max</code>, <code>.length</code>, <code>.nonempty</code></td></tr>
|
|
1623
|
+
<tr><td>Object</td><td><code>t.object(shape)</code>, <code>t.strictObject(shape)</code></td></tr>
|
|
1624
|
+
<tr><td>Object transform</td><td><code>t.extend</code>, <code>t.safeExtend</code>, <code>t.merge</code>, <code>t.pick</code>, <code>t.omit</code>, <code>t.partial</code>, <code>t.deepPartial</code>, <code>t.required</code>, <code>t.strict</code>, <code>t.passthrough</code>, <code>t.strip</code>, <code>t.catchall</code>, 그리고 같은 이름의 object guard method</td></tr>
|
|
1625
|
+
<tr><td>Runtime object contract</td><td><code>t.instanceOf(Ctor)</code>, <code>t.property(base, key, value)</code>, <code>guard.property(key, value)</code></td></tr>
|
|
1626
|
+
<tr><td>Composition</td><td><code>t.union</code>, <code>t.discriminatedUnion</code>, <code>t.intersect</code>, <code>guard.intersect</code></td></tr>
|
|
1627
|
+
<tr><td>Presence</td><td><code>t.optional</code>, <code>t.undefinedable</code>, <code>t.nullable</code>, <code>t.nullish</code></td></tr>
|
|
1628
|
+
<tr><td>Dynamic guard</td><td><code>t.lazy</code>, <code>t.refine</code></td></tr>
|
|
1629
|
+
<tr><td>Decoder</td><td><code>t.decoder</code>, <code>t.transform</code>, <code>t.pipe</code>, <code>t.default</code>, <code>t.defaultValue</code>, <code>t.prefault</code>, <code>t.catch</code>, <code>t.codec</code>, <code>t.coerce</code>, <code>t.string.trim()</code>, <code>t.string.toLowerCase()</code>, <code>t.string.toUpperCase()</code></td></tr>
|
|
1630
|
+
<tr><td>Async decoder</td><td><code>t.asyncDecoder</code>, <code>t.asyncRefine</code>, <code>t.asyncTransform</code>, <code>t.asyncPipe</code></td></tr></tbody></table></div>
|
|
1631
|
+
<p>builder function은 schema가 validation plan, compiler, AOT emitter, diagnostic collector, JSON Schema exporter로 들어가기 전에 입력을 검증합니다. 위조된 guard-like value, 잘못된 schema tag, 잘못된 predicate, 잘못된 bound, malformed regexp, 잘못된 discriminated union case set은 construction 중 거부됩니다.</p>
|
|
1632
|
+
<p>허용된 schema는 저장 전에 freeze됩니다. 공개 schema collection field는 변경 가능한 collection object 대신 frozen array와 frozen key lookup record를 사용합니다.</p>
|
|
1633
|
+
<h3 id="api-reference-ko-객체-key-존재-규칙">객체 key 존재 규칙</h3>
|
|
1634
|
+
<p>TypeSea는 key가 존재하는지와 value domain을 분리합니다.</p>
|
|
1635
|
+
<pre><code>const Shape = t.object({
|
|
1636
|
+
name: t.optional(t.string),
|
|
1637
|
+
nickname: t.undefinedable(t.string)
|
|
1638
|
+
});</code></pre>
|
|
1639
|
+
<ul><li><code>name</code>은 없어도 됩니다. 존재한다면 값은 string이어야 합니다.</li><li><code>nickname</code>은 반드시 존재해야 합니다. 값은 string 또는 <code>undefined</code>일 수 있습니다.</li><li><code>t.nullable(inner)</code>는 value domain에 <code>null</code>을 추가합니다.</li><li><code>t.nullish(inner)</code>는 nullable value와 optional key 의미를 함께 제공합니다.</li><li><code>nullable</code>, <code>undefinedable</code>, <code>brand</code>, <code>refine</code>을 지나도 optional-key 의미는 보존됩니다.</li></ul>
|
|
1640
|
+
<p>object combinator는 object mode를 보존합니다. strict object guard는 <code>extend</code>, <code>pick</code>, <code>omit</code>, <code>partial</code> 이후에도 strict를 유지하고, passthrough object guard는 unknown key 허용을 유지합니다.</p>
|
|
1641
|
+
<p><code>catchall(schema)</code>는 선언되지 않은 모든 own key를 <code>schema</code>로 검증합니다. <code>strip()</code>은 TypeSea에서 검증 전용 의미입니다. guard는 원본 값을 반환하므로, 검증 의미는 <code>passthrough()</code>와 같습니다. <code>pick</code>과 <code>omit</code>은 key array와 Zod 스타일 <code>{ key: true }</code> mask를 모두 받습니다. <code>deepPartial()</code>은 순수 object, array, tuple, tuple rest, record, map, set, property, union, intersection, nullable, undefinedable, optional, brand schema를 재귀적으로 partial 처리합니다. lazy와 refinement schema는 callback 의미를 보존하기 위해 semantic barrier로 둡니다.</p>
|
|
1642
|
+
<p><code>property</code>는 own data descriptor만 검증합니다. 안정적인 class field를 증명할 때 쓰기 좋고, prototype getter나 accessor property는 실행하지 않고 거부합니다.</p>
|
|
1643
|
+
<h3 id="api-reference-ko-합성">합성</h3>
|
|
1644
|
+
<p><code>t.union(a, b)</code>는 적어도 한 branch를 만족하는 값을 허용합니다.</p>
|
|
1645
|
+
<p><code>t.discriminatedUnion("kind", cases)</code>는 string case key를 요구합니다. 각 case는 static하게 inspect할 수 있는 object case여야 하며, dispatch key는 case name과 일치하는 required string literal이어야 합니다.</p>
|
|
1646
|
+
<p><code>t.intersect(a, b)</code>와 <code>guard.intersect(other)</code>는 같은 input value가 두 guard를 모두 만족해야 합니다. <code>check()</code>는 양쪽 diagnostic을 모두 수집합니다.</p>
|
|
1647
|
+
<h3 id="api-reference-ko-재귀">재귀</h3>
|
|
1648
|
+
<p>recursive contract는 반드시 <code>t.lazy</code>를 사용해야 합니다.</p>
|
|
1649
|
+
<pre><code>interface ListNode {
|
|
1650
|
+
readonly value: string;
|
|
1651
|
+
readonly next?: ListNode;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const Node: Guard<ListNode> = t.lazy((): Guard<ListNode> =>
|
|
1655
|
+
t.object({
|
|
1656
|
+
value: t.string,
|
|
1657
|
+
next: t.optional(Node)
|
|
1658
|
+
})
|
|
1659
|
+
);</code></pre>
|
|
1660
|
+
<p>직접 순환하는 schema object는 builder boundary에서 거부됩니다. lazy guard는 guard instance마다 한 번 resolve되고 recursive schema identity를 안정적으로 유지합니다. lazy chain은 결국 concrete non-lazy schema로 resolve되어야 합니다.</p>
|
|
1661
|
+
<h3 id="api-reference-ko-decoder-pipeline">Decoder Pipeline</h3>
|
|
1662
|
+
<pre><code>const Count = t.pipe(t.coerce.number(), t.number.int().gte(0));
|
|
1663
|
+
const result = Count.decode("42");
|
|
1664
|
+
|
|
1665
|
+
const Name = t.default(t.string.min(1), "anonymous");
|
|
1666
|
+
const NumberText = t.codec(
|
|
1667
|
+
t.string.regex(/^\d+$/u, "digits"),
|
|
1668
|
+
t.number.int().nonnegative(),
|
|
1669
|
+
{
|
|
1670
|
+
decode: (value) => Number(value),
|
|
1671
|
+
encode: (value) => String(value)
|
|
1672
|
+
}
|
|
1673
|
+
);</code></pre>
|
|
1674
|
+
<p>decoder는 output을 생성하는 작업에 씁니다. <code>decode()</code>에서 <code>Result</code>를 반환하며 <code>is()</code> predicate를 노출하지 않습니다. decoded output이 input과 같은 runtime value가 아닐 수 있기 때문입니다.</p>
|
|
1675
|
+
<ul><li><code>t.transform(source, mapper)</code>는 <code>source</code>를 decode한 뒤 decoded value를 map합니다.</li><li><code>t.pipe(source, next)</code>는 성공한 decoded value를 다음 guard 또는 decoder에 넘깁니다.</li><li><code>t.default(source, value)</code>는 input이 <code>undefined</code>일 때 fallback output을 바로 반환합니다.</li><li><code>t.prefault(source, value)</code>는 input이 <code>undefined</code>일 때 fallback input을 source에 다시 통과시킵니다.</li><li><code>t.codec(input, output, mapping)</code>은 bidirectional decode/encode 양쪽을 모두 검증합니다.</li><li><code>t.coerce.string</code>, <code>t.coerce.number</code>, <code>t.coerce.boolean</code>은 명시적 primitive coercion을 제공합니다.</li><li><code>t.string.trim()</code>, <code>t.string.toLowerCase()</code>, <code>t.string.toUpperCase()</code>는 decoder helper입니다. 먼저 string을 검증한 뒤 <code>decode()</code> 결과로 변환된 값을 반환합니다.</li><li><code>t.asyncRefine</code>, <code>t.asyncTransform</code>, <code>t.asyncPipe</code>는 <code>decodeAsync()</code>에서 <code>Promise<Result<T, Issue[]>></code>를 반환합니다.</li></ul>
|
|
1676
|
+
<p>예상 가능한 async validation 실패도 <code>Result</code>로 반환됩니다.</p>
|
|
1677
|
+
<h3 id="api-reference-ko-message">Message</h3>
|
|
1678
|
+
<pre><code>const checked = withMessages(User.check(input), {
|
|
1679
|
+
locale: "ko",
|
|
1680
|
+
catalog: defineMessages({
|
|
1681
|
+
expected_string: "{path}: 문자열 필요"
|
|
1682
|
+
})
|
|
1683
|
+
});</code></pre>
|
|
1684
|
+
<p><code>formatIssue</code>, <code>formatIssues</code>, <code>flattenIssues</code>, <code>withMessages</code>는 validation이 끝난 뒤 diagnostic을 렌더링합니다. 따라서 <code>is()</code>와 일반 <code>check()</code> path에서는 message allocation이 발생하지 않습니다.</p>
|
|
1685
|
+
<p>built-in locale은 <code>en</code>과 <code>ko</code>입니다. custom catalog는 <code>{path}</code>, <code>{code}</code>, <code>{expected}</code>, <code>{actual}</code> string template 또는 formatter callback을 쓸 수 있습니다. <code>withMessages(result, options)</code>는 successful result를 그대로 보존하고, failed <code>Result</code>에는 복사되고 동결된 issue에 <code>message</code> field를 채워 새로 반환합니다. <code>flattenIssues(issues, options)</code>는 렌더링된 message를 <code>formErrors</code>와 top-level <code>fieldErrors</code> bucket으로 묶습니다.</p>
|
|
1686
|
+
<h3 id="api-reference-ko-런타임-컴파일">런타임 컴파일</h3>
|
|
1687
|
+
<pre><code>const FastUser = compile(User, { name: "isUser" });
|
|
1688
|
+
|
|
1689
|
+
FastUser.is(input);
|
|
1690
|
+
FastUser.check(input);</code></pre>
|
|
1691
|
+
<p><code>compile</code>은 optimized Sea-of-Nodes validation graph에서 generated predicate function과 failed value용 diagnostic collector를 방출합니다. static scalar, object, array, record, union, strict-key node는 가능한 경우 straight-line JavaScript 또는 indexed loop로 낮아집니다. <code>lazy</code>, <code>refine</code> 같은 dynamic schema edge는 ordinary guard execution과 같은 IR-backed runtime fallback을 사용해 의미를 유지합니다.</p>
|
|
1692
|
+
<p>선택적 <code>name</code>은 debugging과 profiling을 위한 hint입니다. TypeSea는 이를 strict-mode-safe JavaScript function name으로 normalize하고, reserved name에는 prefix를 붙이며, generated name 길이에 cap을 둡니다. 직접 compiled guard construction은 predicate, collector, source argument를 검증합니다. collector diagnostic은 <code>check()</code> 반환 전에 validate, copy, freeze됩니다.</p>
|
|
1693
|
+
<p>generated source는 사용자가 제어하는 값을 직접 interpolate하지 않습니다. literal, regexp, property key, keyset, dynamic schema fallback은 side table에 capture되고 numeric index로 참조됩니다.</p>
|
|
1694
|
+
<h4 id="api-reference-ko-unsafe-컴파일-모드">Unsafe 컴파일 모드</h4>
|
|
1695
|
+
<pre><code>const FastButLooseUser = compile(User, {
|
|
1696
|
+
name: "isUserFast",
|
|
1697
|
+
mode: "unsafe"
|
|
1698
|
+
});</code></pre>
|
|
1699
|
+
<p><code>CompileOptions["mode"]</code>와 <code>AotCompileOptions["mode"]</code>는 <code>"safe" | "unsafe" | "unchecked" | undefined</code>입니다. option을 생략하면 <code>"safe"</code>가 기본입니다. safe mode는 TypeSea의 적대적 입력 방어 계약을 유지합니다. descriptor 기반 property read, getter 실행 금지, symbol과 non-enumerable extra를 포함한 strict-object rejection을 보장합니다.</p>
|
|
1700
|
+
<p>unsafe mode는 신뢰할 수 있고 정규화된 plain data를 위한 명시적 performance escape hatch입니다.</p>
|
|
1701
|
+
<ul><li>field schema가 <code>undefined</code>를 거부하는 required object field는 <code>value[key]</code>로 읽습니다.</li><li>discriminant dispatch는 tag를 direct bracket access로 읽습니다.</li><li>array와 tuple은 direct indexed load를 사용합니다.</li><li>strict-object extra-key rejection은 allocation-free own-enumerable <code>for...in</code> loop를 사용합니다.</li></ul>
|
|
1702
|
+
<p>이 모드는 getter를 실행할 수 있고, prototype-backed value를 받아들일 수 있으며, strict object에서 symbol 또는 non-enumerable extra를 거부하지 않습니다. compiled <code>check()</code>는 먼저 generated predicate의 판정을 신뢰하므로, unsafe predicate가 <code>true</code>를 반환하면 <code>check()</code>도 successful result를 반환합니다. input이 trusted normalization boundary를 지난 뒤에만 unsafe mode를 사용하세요.</p>
|
|
1703
|
+
<p>unsafe mode는 escaped static property key를 generated predicate source에 직접 넣을 수 있습니다. 그래야 V8이 ordinary property-load inline cache를 붙이기 쉽습니다. safe mode는 property key를 side table에 유지합니다.</p>
|
|
1704
|
+
<p>unchecked mode는 unsafe direct-read shape을 사용하고 strict-object extra-key loop도 건너뜁니다. object shape이 이미 신뢰되거나 정규화된 input에만 사용해야 합니다. 이 모드에서는 strict object가 더 이상 extra key를 거부하지 않습니다.</p>
|
|
1705
|
+
<p>unsafe와 unchecked compiled <code>check()</code>는 successful Result object를 <code>Object.freeze()</code> 없이 raw object로 반환합니다. failure diagnostic은 계속 freeze됩니다. safe mode는 success와 failure 모두 frozen Result object를 유지합니다. FastMode diagnostic collector는 가능한 경우 direct field read와 FastMode strict-key rule을 사용합니다. 따라서 missing/accessor issue code는 safe mode와 일치한다고 보장하지 않습니다. array와 tuple diagnostic도 fast mode에서는 direct indexed read를 쓰므로 sparse slot은 loaded <code>undefined</code> value 기준으로 진단됩니다. record diagnostic은 direct <code>record[key]</code> read를 사용합니다. unchecked mode는 inherited enumerable key도 방문합니다. discriminant diagnostic은 tag를 직접 읽고 string case를 <code>===</code>로 비교합니다.</p>
|
|
1706
|
+
<h3 id="api-reference-ko-aot-모듈-생성">AOT 모듈 생성</h3>
|
|
1707
|
+
<pre><code>const emitted = emitAotModule(User, { name: "aotUser" });
|
|
1708
|
+
const unsafeEmitted = emitAotModule(User, {
|
|
1709
|
+
name: "aotUserFast",
|
|
1710
|
+
mode: "unsafe"
|
|
1711
|
+
});
|
|
1712
|
+
const uncheckedEmitted = emitAotModule(User, {
|
|
1713
|
+
name: "aotUserTrustedShape",
|
|
1714
|
+
mode: "unchecked"
|
|
1715
|
+
});</code></pre>
|
|
1716
|
+
<p><code>emitAotModule</code>은 <code>Result<AotModule, AotIssue[]></code>를 반환합니다. successful result에는 standalone ESM validator source와 declaration source가 들어 있습니다. generated module은 module load time에 dynamic source compilation을 요구하지 않고 <code>is</code>, <code>check</code>, <code>assert</code>, default frozen guard-like object를 export합니다.</p>
|
|
1717
|
+
<p>AOT generation은 lossless-only입니다. runtime callback 또는 serialize할 수 없는 identity가 필요한 schema는 명시적 AOT issue를 반환합니다.</p>
|
|
1718
|
+
<h3 id="api-reference-ko-framework-adapter">Framework Adapter</h3>
|
|
1719
|
+
<pre><code>const parser = toTrpcParser(User);
|
|
1720
|
+
const routeSchema = toFastifyRouteSchema(User);
|
|
1721
|
+
const validatorCompiler = toFastifyValidatorCompiler(User);
|
|
1722
|
+
const resolver = toReactHookFormResolver(User);</code></pre>
|
|
1723
|
+
<p>adapter는 구조적으로 맞춰진 얇은 연결 계층이며 런타임 의존성이 없습니다. TypeSea는 tRPC, Fastify, React Hook Form을 import하지 않습니다.</p>
|
|
1724
|
+
<p>compiled guard도 같은 adapter에 넘길 수 있습니다. hot request path에서는 이 형태를 권장합니다. startup에서 한 번 compile한 뒤 adapter가 generated predicate를 재사용하게 하세요.</p>
|
|
1725
|
+
<pre><code>const FastUser = compile(User);
|
|
1726
|
+
const fastParser = toTrpcParser(FastUser);
|
|
1727
|
+
const fastValidatorCompiler = toFastifyValidatorCompiler(FastUser);</code></pre>
|
|
1728
|
+
<p>public input boundary에서는 기본 compiled mode를 쓰세요. 신뢰된, 이미 정규화된 내부 데이터에서는 더 빠른 mode를 같은 방식으로 adapter에 연결할 수 있습니다.</p>
|
|
1729
|
+
<pre><code>const UnsafeUser = compile(User, { mode: "unsafe" });
|
|
1730
|
+
const internalParser = toTrpcParser(UnsafeUser);
|
|
1731
|
+
|
|
1732
|
+
const TrustedShapeUser = compile(User, { mode: "unchecked" });
|
|
1733
|
+
const internalValidatorCompiler = toFastifyValidatorCompiler(TrustedShapeUser);</code></pre>
|
|
1734
|
+
<div class="table-wrap"><table><thead><tr><th>Adapter</th><th>Export</th><th>동작</th></tr></thead><tbody><tr><td>tRPC</td><td><code>toTrpcParser</code>, <code>toAsyncTrpcParser</code></td><td>decoded value를 반환하거나 <code>TypeSeaAssertionError</code>를 던지는 parser object를 반환합니다.</td></tr>
|
|
1735
|
+
<tr><td>Fastify route schema</td><td><code>toFastifyRouteSchema</code></td><td>guard를 JSON Schema route fragment로 변환합니다.</td></tr>
|
|
1736
|
+
<tr><td>Fastify validator compiler</td><td><code>toFastifyValidatorCompiler</code></td><td><code>{ value }</code> 또는 <code>{ error }</code>를 만드는 compiler-shaped validator를 반환합니다.</td></tr>
|
|
1737
|
+
<tr><td>React Hook Form</td><td><code>toReactHookFormResolver</code></td><td>TypeSea message를 field error로 매핑하는 async resolver를 반환합니다.</td></tr></tbody></table></div>
|
|
1738
|
+
<h3 id="api-reference-ko-graph-and-ir">Graph and IR</h3>
|
|
1739
|
+
<pre><code>const graph = User.graph();
|
|
1740
|
+
const optimized = optimizeGraph(graph);</code></pre>
|
|
1741
|
+
<p><code>Guard.graph()</code>는 runtime validation plan이 보유한 optimized Sea-of-Nodes validation graph를 반환합니다. 같은 plan은 <code>is()</code>가 사용하는 specialized predicate kernel도 소유합니다. graph는 <code>compile()</code>과 <code>emitAotModule()</code>의 source이고, kernel은 ordinary guard execution이 generic per-node interpreter를 타지 않게 합니다. 공개 graph value는 validate, dependency-check, dense compaction, freeze를 거쳐 반환됩니다.</p>
|
|
1742
|
+
<p><code>optimizeGraph(graph)</code>는 직접 전달된 graph input을 validate한 뒤 optimize합니다. regex graph node는 plain <code>RegExp</code> value만 받으며, graph가 freeze되기 전에 extensible input을 clone해서 non-extensible regexp로 저장합니다.</p>
|
|
1743
|
+
<p><code>SchemaCheck</code>는 <code>lazy</code>나 <code>refine</code>처럼 dynamic runtime schema logic을 기록합니다. callback-backed edge를 static primitive인 척하지 않고, runtime semantics가 필요하다는 사실을 IR에 정확히 남깁니다.</p>
|
|
1744
|
+
<h3 id="api-reference-ko-json-schema-내보내기">JSON Schema 내보내기</h3>
|
|
1745
|
+
<pre><code>const result = toJsonSchema(User);</code></pre>
|
|
1746
|
+
<p><code>toJsonSchema</code>는 <code>Result<JsonSchema, JsonSchemaExportIssue[]></code>를 반환합니다. TypeSea가 JSON-compatible input value 위에서 contract를 의미 손실 없이 표현할 수 있을 때만 성공합니다.</p>
|
|
1747
|
+
<p>runtime-only concept는 명시적 export issue를 반환합니다.</p>
|
|
1748
|
+
<ul><li><code>undefined</code></li><li><code>bigint</code></li><li><code>symbol</code></li><li>JavaScript <code>Date</code>, <code>Map</code>, <code>Set</code>, <code>instanceOf</code>, <code>property</code> contract</li><li><code>lazy</code></li><li><code>refine</code></li><li>decoder transforms</li><li>async validation</li><li>flag가 있는 regexp</li><li><code>NaN</code>, <code>Infinity</code>, <code>-0</code>처럼 JSON이 보존할 수 없는 numeric literal</li></ul>
|
|
1749
|
+
<p><code>schemaToJsonSchema(schema)</code>는 direct schema API입니다. 전달된 schema를 validate하고 freeze한 뒤 export합니다. JSON Schema option도 validate합니다. <code>schemaId</code>가 있으면 string이어야 합니다.</p>
|
|
1750
|
+
<p>object <code>properties</code> map은 null-prototype record로 방출됩니다. 따라서 <code>__proto__</code>, <code>constructor</code>, <code>hasOwnProperty</code> 같은 특수 key도 ordinary own schema property로 남습니다.</p>
|
|
1751
|
+
<h3 id="api-reference-ko-경계-동작">경계 동작</h3>
|
|
1752
|
+
<ul><li>literal guard는 <code>Object.is</code>를 사용합니다. 따라서 <code>t.literal(Number.NaN)</code>은 <code>NaN</code>을 match하고 <code>t.literal(-0)</code>은 <code>0</code>과 match하지 않습니다.</li><li><code>t.number</code>는 finite JavaScript number만 허용합니다. <code>NaN</code>, <code>Infinity</code>, <code>-Infinity</code>는 configured numeric predicate가 실행되기 전에 거부됩니다.</li><li>string length bound는 non-negative integer여야 합니다.</li><li>numeric comparison bound는 finite number여야 합니다.</li><li>predicate callback은 strict <code>true</code>를 반환해야 합니다. truthy non-boolean value는 validation을 통과하지 않습니다.</li><li><code>RegExp</code> check는 매 test 전에 <code>lastIndex</code>를 reset합니다. global과 sticky regexp의 상태가 validation 사이에 새지 않습니다.</li><li>string regex builder와 direct string regex schema는 plain <code>RegExp</code> instance만 받습니다. 허용된 regex check는 storage 전에 clone됩니다.</li></ul>
|
|
1753
|
+
<h3 id="api-reference-ko-result-계약">Result 계약</h3>
|
|
1754
|
+
<pre><code>type Result<T, E> =
|
|
1755
|
+
| { ok: true; value: T }
|
|
1756
|
+
| { ok: false; error: E };</code></pre>
|
|
1757
|
+
<p>예상 가능한 validation failure는 <code>Result</code>를 사용합니다. Result container는 runtime에서 freeze됩니다. successful value는 caller-owned data이므로 recursive freeze하지 않습니다.</p>
|
|
1758
|
+
</div>
|
|
1759
|
+
</article>
|
|
1760
|
+
<article id="engine-notes" class="source-doc doc-content" data-doc-section>
|
|
1761
|
+
<header>
|
|
1762
|
+
<p class="eyebrow">
|
|
1763
|
+
<span class="i18n-en">Engine Notes</span>
|
|
1764
|
+
<span class="i18n-ko" lang="ko">엔진 설계 노트</span>
|
|
1765
|
+
</p>
|
|
1766
|
+
<h2>
|
|
1767
|
+
<span class="i18n-en">Engine Notes</span>
|
|
1768
|
+
<span class="i18n-ko" lang="ko">엔진 설계 노트</span>
|
|
1769
|
+
</h2>
|
|
1770
|
+
<p>
|
|
1771
|
+
<span class="i18n-en">Rendered from <code>docs/engine-notes.md</code>.</span>
|
|
1772
|
+
<span class="i18n-ko" lang="ko"><code>docs/ko/engine-notes.md</code>에서 렌더링했습니다.</span>
|
|
1773
|
+
</p>
|
|
1774
|
+
</header>
|
|
1775
|
+
<div class="locale-en">
|
|
1776
|
+
<h2 id="engine-notes-en-engine-notes">Engine Notes</h2>
|
|
1777
|
+
<p>TypeSea is written for predictable machine behavior after TypeScript emits JavaScript. The goal is not obscurity; the goal is to make object shapes, allocation sites, branch behavior, and validation contracts visible in code.</p>
|
|
1778
|
+
<h3 id="engine-notes-en-hot-path-rules">Hot Path Rules</h3>
|
|
1779
|
+
<ul><li>Use prototype methods instead of per-instance method closures.</li><li>Use numeric tags for schema, check, issue, and IR node variants.</li><li>Initialize class fields in one constructor order.</li><li>Keep successful <code>is()</code> validation free of diagnostic allocation.</li><li>Allocate <code>Issue</code> objects and path arrays only when diagnostics are requested.</li><li>Prefer indexed loops on recursive validation paths.</li><li>Precompute object-entry arrays during schema construction.</li><li>After required object fields have proved their data-property descriptors, load</li></ul>
|
|
1780
|
+
<p>descriptor values directly instead of rechecking missing-property fallbacks.</p>
|
|
1781
|
+
<ul><li>For all-required strict objects, reject extras by counting own string names</li></ul>
|
|
1782
|
+
<p>and own symbols after field validation. Optional strict objects keep the full key membership scan.</p>
|
|
1783
|
+
<ul><li>Keep <code>compile()</code> and <code>emitAotModule()</code> safe by default. Unsafe mode is an</li></ul>
|
|
1784
|
+
<p>explicit opt-in that may use direct property/index loads and own-enumerable strict-key loops after the caller accepts getter/prototype/symbol-extra risk.</p>
|
|
1785
|
+
<ul><li>Mark constructed guards out-of-band so normal receivers avoid repeated schema</li></ul>
|
|
1786
|
+
<p>validation while forged receivers still fall back to structural checks.</p>
|
|
1787
|
+
<ul><li>Use <code>Readonly<Record<string, unknown>></code> after object guards.</li><li>Store generated-validator literals, regexps, keysets, and dynamic fallbacks in</li></ul>
|
|
1788
|
+
<p>side tables instead of interpolating user-controlled values into source text.</p>
|
|
1789
|
+
<h3 id="engine-notes-en-type-system-rules">Type-System Rules</h3>
|
|
1790
|
+
<ul><li><code>optional(inner)</code> means an object key may be absent.</li><li><code>undefinedable(inner)</code> means the key must exist when used in an object shape,</li></ul>
|
|
1791
|
+
<p>but its value may be <code>undefined</code>.</p>
|
|
1792
|
+
<ul><li><code>nullable(inner)</code> means the value may be <code>null</code>.</li><li>Presence-preserving wrappers do not erase optional-key semantics.</li><li><code>number</code> means finite JavaScript number.</li><li><code>unknown</code> is the only accepted boundary type for untrusted input.</li><li>Builder validation is the hard barrier before a schema reaches the engine.</li></ul>
|
|
1793
|
+
<h3 id="engine-notes-en-ir-rules">IR Rules</h3>
|
|
1794
|
+
<p>The public schema tree is the semantic source used by builders and diagnostic collectors. Boolean validation executes a cached <code>ValidationPlan</code>: schema identity is lowered into Sea-of-Nodes IR, the optimizer runs, and the plan keeps both the frozen graph and a schema-specialized predicate kernel.</p>
|
|
1795
|
+
<p>The graph is not decorative. <code>compile()</code>, AOT emission, and <code>Guard.graph()</code> all consume the optimized graph held by the plan. Ordinary <code>Guard.is()</code> deliberately uses the sibling schema-specialized kernel instead of a generic node interpreter, because per-node dispatch and scratch-slot bookkeeping cost more than they buy on the most common hot path.</p>
|
|
1796
|
+
<p>Current lowering hash-conses pure value and predicate nodes. Strict object schemas lower an explicit keyset check into the IR, so extra-key rejection does not depend on out-of-band schema knowledge. Required and optional object fields separate key presence from data-property presence, which keeps accessor-backed properties from executing getters or being misclassified as valid values.</p>
|
|
1797
|
+
<p><code>Guard.graph()</code> returns the same optimized graph held by the validation plan. Public graph values are validated and frozen before leaving the API. The first optimizer pass performs reachable node elimination and compacts node ids so every dependency points at an existing dense node index. <code>compile()</code> and AOT emission use this graph as their predicate source.</p>
|
|
1798
|
+
<p>Array, tuple, and record schemas lower to native composite IR nodes whose child schemas are executed through child validation plans. <code>SchemaCheck</code> is reserved for dynamic schemas such as <code>lazy</code> and <code>refine</code>; the graph records that callback or resolver-backed semantics are required instead of pretending they are static predicates.</p>
|
|
1799
|
+
<h3 id="engine-notes-en-runtime-compiler">Runtime Compiler</h3>
|
|
1800
|
+
<p>Compiled guards emit boolean predicates from optimized Sea-of-Nodes graphs and schema-aware diagnostics collectors for failed values. Runtime <code>is()</code> uses the plan-owned schema-specialized kernel to avoid recursive node dispatch and scratch buffer churn. <code>check()</code> first asks the plan predicate for the pass/fail verdict; successful values skip diagnostic collection, while failed values replay the diagnostic collector to build paths and issue codes.</p>
|
|
1801
|
+
<p>User-controlled literals, regexps, object keys, keysets, dynamic schemas, and diagnostic names live in side tables captured by the generated factory. The generated source contains numeric side-table indexes, fixed helper strings, and sanitized function names.</p>
|
|
1802
|
+
<p>Scalar nodes emit direct JavaScript tests where the semantics are local: finite-number checks, integer checks, string length bounds, literal equality, and regexp tests all lower without helper calls on the generated hot path.</p>
|
|
1803
|
+
<p>Array and record IR nodes emit indexed loops. Static child schemas are inlined into those loops from their optimized graphs, which avoids function-call boundaries for small scalar or union element contracts. Tuple nodes preserve descriptor-based element access, and dynamic edges use the same IR-backed runtime fallback as ordinary guard execution, preserving behavior for <code>lazy</code> and <code>refine</code>.</p>
|
|
1804
|
+
<p>Strict object IR emits two shapes. When every declared key is required, generated validators run the strict-key count before field descriptor reads: they compare <code>Object.getOwnPropertyNames(value).length</code> with the declared key count and require <code>Object.getOwnPropertySymbols(value).length === 0</code>. V8 optimizes this count-only path better than a generic <code>Reflect.ownKeys</code> count, and it rejects obvious extra-key objects before touching field descriptors. Optional strict objects still emit the full own-key membership scan because a missing optional key cannot be distinguished by the final key count alone.</p>
|
|
1805
|
+
<p><code>compile(..., { mode: "unsafe" })</code> and <code>emitAotModule(..., { mode: "unsafe" })</code> switch generated predicates to a trusted-data code shape. Required object fields whose schemas reject <code>undefined</code> use direct <code>value[key]</code> loads without descriptor or own-key checks. Required fields that can accept <code>undefined</code> retain an own-key presence guard so missing required keys do not collapse into valid <code>undefined</code> values. Optional fields take the direct-load fast path for present non-<code>undefined</code> values and fall back to an own-key check only for the ambiguous <code>undefined</code> case.</p>
|
|
1806
|
+
<p>Unsafe array, tuple, record, and discriminant paths also prefer direct loads. Strict objects use a <code>for...in</code> own-enumerable key loop instead of allocating own-key arrays. Object keys that are ASCII identifier names emit as dot-property loads such as <code>value.id</code>; other keys emit as escaped string-literal bracket loads. That is intentionally not hostile-input equivalent: getters can execute, prototype-backed values can be accepted, symbol or non-enumerable strict extras are not rejected, and static property names may appear in unsafe generated predicate source.</p>
|
|
1807
|
+
<p><code>mode: "unchecked"</code> keeps the unsafe direct-read shape and removes strict extra-key loops. It is a trusted-shape path for objects already normalized by the caller; strict objects no longer reject any extra keys there.</p>
|
|
1808
|
+
<p>Fast modes also remove <code>Object.freeze()</code> from successful compiled <code>check()</code> results. The returned object keeps the same <code>{ ok: true, value }</code> shape, but it is intentionally not frozen. Failed diagnostics stay frozen because those objects are off the success hot path and are often retained for reporting. Object diagnostics in fast modes are generated from the same direct-read contract as predicates. Required fields load through <code>value.key</code>, optional fields use direct load plus an own-key fallback for <code>undefined</code>, unsafe strict objects scan own enumerable string keys, and unchecked strict objects skip the strict-key diagnostic scan. Array and tuple diagnostics in fast modes read items through direct indexes instead of descriptor probes. Record diagnostics read through <code>record[key]</code>; unchecked mode intentionally keeps inherited enumerable keys visible. Discriminant diagnostics read the tag directly and compare literal string cases with strict equality.</p>
|
|
1809
|
+
<h3 id="engine-notes-en-recursion">Recursion</h3>
|
|
1810
|
+
<p>Lazy schemas resolve their getter once per guard instance. Recursive validation therefore sees stable schema identity, and repeated validations do not rebuild the recursive schema graph.</p>
|
|
1811
|
+
<p>Recursive validation uses a root-local active pair table keyed by runtime object identity and schema identity. Re-entering the same schema/value pair short-circuits that edge, which lets cyclic object graphs validate finitely while still checking the original object fields on the outer frame.</p>
|
|
1812
|
+
<p>Compiled <code>lazy</code> and <code>refine</code> fallbacks use the same IR-backed runtime path, so recursive behavior stays consistent across execution engines.</p>
|
|
1813
|
+
<p><code>checkFirst()</code> has a separate generated collector. It returns one frozen issue as soon as the first diagnostic is known, instead of running the full <code>check()</code> collector and truncating its issue array.</p>
|
|
1814
|
+
<h3 id="engine-notes-en-json-schema-export">JSON Schema Export</h3>
|
|
1815
|
+
<p>JSON Schema export succeeds only when the TypeSea schema can be represented over JSON-compatible input values without semantic loss. Runtime-only concepts return typed <code>Result</code> errors.</p>
|
|
1816
|
+
<p>Export diagnostics keep paths at the failed child slot instead of collapsing everything to the parent container. Nested unsupported schemas therefore remain actionable without reconstructing the schema tree manually.</p>
|
|
1817
|
+
<p>Literal checks use <code>Object.is</code> in runtime-plan and compiled paths. Diagnostics use the same literal formatting, including <code>-0</code>, so compiled and runtime-plan <code>check()</code> results stay byte-for-byte comparable in tests.</p>
|
|
1818
|
+
<h3 id="engine-notes-en-benchmark-scope">Benchmark Scope</h3>
|
|
1819
|
+
<p>The benchmark suite keeps two questions separate:</p>
|
|
1820
|
+
<ul><li><code>compile.bench.ts</code> compares TypeSea runtime-plan and compiled validators over</li></ul>
|
|
1821
|
+
<p>the same TypeSea schema.</p>
|
|
1822
|
+
<ul><li><code>ecosystem.bench.ts</code> compares TypeSea runtime-plan, TypeSea compiled, Zod,</li></ul>
|
|
1823
|
+
<p>Valibot, and Ajv over one JSON-compatible strict-object contract.</p>
|
|
1824
|
+
<p>Zod, Valibot, and Ajv are dev dependencies for measurement only. They are not imported by <code>src</code>, and package policy rejects runtime, peer, optional, or bundled dependency fields before release.</p>
|
|
1825
|
+
<p>Last local benchmark on 2026-07-04 KST reported these ecosystem paths over the JSON-compatible strict-object benchmark:</p>
|
|
1826
|
+
<div class="table-wrap"><table><thead><tr><th>Case</th><th>TypeSea runtime plan</th><th>TypeSea compiled safe</th><th>TypeSea compiled unsafe</th><th>TypeSea compiled unchecked</th><th>Ajv compiled</th></tr></thead><tbody><tr><td>Valid <code>is()</code></td><td>513,701 hz</td><td>4,297,306 hz</td><td>36,297,653 hz</td><td>42,581,174 hz</td><td>4,275,389 hz</td></tr>
|
|
1827
|
+
<tr><td>Valid <code>check()</code></td><td>503,232 hz</td><td>3,903,929 hz</td><td>35,568,425 hz</td><td>40,084,605 hz</td><td>4,278,587 hz</td></tr>
|
|
1828
|
+
<tr><td>Invalid <code>is()</code></td><td>3,636,369 hz</td><td>42,080,241 hz</td><td>49,654,076 hz</td><td>50,482,732 hz</td><td>27,820,643 hz</td></tr>
|
|
1829
|
+
<tr><td>Invalid <code>check()</code></td><td>420,446 hz</td><td>2,086,129 hz</td><td>3,077,367 hz</td><td>3,673,508 hz</td><td>28,713,035 hz</td></tr></tbody></table></div>
|
|
1830
|
+
<p>Benchmark numbers are machine-local telemetry. They are useful for catching regressions, not for promising a fixed throughput floor. Unsafe and unchecked numbers are not hostile-input equivalent to safe mode.</p>
|
|
1831
|
+
</div>
|
|
1832
|
+
<div class="locale-ko" lang="ko">
|
|
1833
|
+
<h2 id="engine-notes-ko-엔진-설계-노트">엔진 설계 노트</h2>
|
|
1834
|
+
<p>TypeSea는 TypeScript가 JavaScript를 emit한 뒤의 실행 특성을 예측 가능하게 만들기 위해 작성했습니다. 목표는 난해한 코드가 아닙니다. object shape, allocation site, branch behavior, validation contract가 코드에서 드러나도록 만드는 것이 목표입니다.</p>
|
|
1835
|
+
<h3 id="engine-notes-ko-hot-path-규칙">Hot Path 규칙</h3>
|
|
1836
|
+
<ul><li>per-instance method closure 대신 prototype method를 사용합니다.</li><li>schema, check, issue, IR node variant에는 numeric tag를 씁니다.</li><li>class field는 모든 constructor에서 같은 순서로 초기화합니다.</li><li>successful <code>is()</code> validation은 diagnostic allocation을 만들지 않습니다.</li><li><code>Issue</code> object와 path array는 diagnostic이 요청될 때만 할당합니다.</li><li>recursive validation path에서는 indexed loop를 선호합니다.</li><li>object-entry array는 schema construction 중 미리 계산합니다.</li><li>required object field가 data-property descriptor임을 증명한 뒤에는 missing-property fallback을 다시 검사하지 않고 descriptor value를 직접 읽습니다.</li><li>모든 field가 required인 strict object는 field validation 뒤 own string name과 own symbol 수를 세어 extra를 거부합니다. optional strict object는 전체 key membership scan을 유지합니다.</li><li><code>compile()</code>과 <code>emitAotModule()</code>은 safe가 기본입니다. unsafe mode는 명시적 opt-in이며, caller가 getter, prototype, symbol-extra risk를 받아들였을 때 direct property/index load와 own-enumerable strict-key loop를 쓸 수 있습니다.</li><li>constructed guard는 out-of-band로 표시합니다. 정상 receiver는 반복 schema validation을 피하고, forged receiver는 structural check로 fallback합니다.</li><li>object guard 뒤에는 <code>Readonly<Record<string, unknown>></code>을 사용합니다.</li><li>generated-validator literal, regexp, keyset, dynamic fallback은 사용자가 제어하는 값을 source text에 interpolate하지 않고 side table에 저장합니다.</li></ul>
|
|
1837
|
+
<h3 id="engine-notes-ko-타입-시스템-규칙">타입 시스템 규칙</h3>
|
|
1838
|
+
<ul><li><code>optional(inner)</code>은 object key가 없어도 된다는 뜻입니다.</li><li><code>undefinedable(inner)</code>은 object shape에서 key가 반드시 존재하지만 value가 <code>undefined</code>일 수 있다는 뜻입니다.</li><li><code>nullable(inner)</code>은 value가 <code>null</code>일 수 있다는 뜻입니다.</li><li>presence-preserving wrapper는 optional-key semantics를 지우지 않습니다.</li><li><code>number</code>는 finite JavaScript number를 의미합니다.</li><li>신뢰할 수 없는 입력의 boundary type은 <code>unknown</code>만 허용합니다.</li><li>builder validation은 schema가 engine에 도달하기 전의 hard barrier입니다.</li></ul>
|
|
1839
|
+
<h3 id="engine-notes-ko-ir-규칙">IR 규칙</h3>
|
|
1840
|
+
<p>public schema tree는 builder와 diagnostic collector가 사용하는 semantic source입니다. boolean validation은 cached <code>ValidationPlan</code>을 실행합니다. schema identity는 Sea-of-Nodes IR로 낮아지고 optimizer를 거치며, plan은 frozen graph와 schema-specialized predicate kernel을 함께 보유합니다.</p>
|
|
1841
|
+
<p>graph는 장식이 아닙니다. <code>compile()</code>, AOT emission, <code>Guard.graph()</code>는 모두 plan이 보유한 optimized graph를 소비합니다. ordinary <code>Guard.is()</code>는 generic node interpreter 대신 sibling schema-specialized kernel을 의도적으로 사용합니다. 가장 흔한 hot path에서는 per-node dispatch와 scratch-slot bookkeeping 비용이 이득보다 크기 때문입니다.</p>
|
|
1842
|
+
<p>현재 lowering은 pure value node와 predicate node를 hash-cons합니다. strict object schema는 explicit keyset check를 IR로 낮춥니다. 따라서 extra-key rejection이 out-of-band schema knowledge에 의존하지 않습니다. required object field와 optional object field는 key presence와 data-property presence를 분리합니다. 이 방식은 accessor-backed property의 getter를 실행하거나 valid value로 잘못 분류하지 않게 합니다.</p>
|
|
1843
|
+
<p><code>Guard.graph()</code>는 validation plan이 보유한 동일한 optimized graph를 반환합니다. public graph value는 API 밖으로 나가기 전에 validate되고 freeze됩니다. 첫 optimizer pass는 reachable node elimination을 수행하고 node id를 compact해서 모든 dependency가 존재하는 dense node index를 가리키게 합니다. <code>compile()</code>과 AOT emission은 이 graph를 predicate source로 사용합니다.</p>
|
|
1844
|
+
<p>array, tuple, record schema는 native composite IR node로 낮아지며, child schema는 child validation plan을 통해 실행됩니다. <code>SchemaCheck</code>는 <code>lazy</code>와 <code>refine</code> 같은 dynamic schema에 예약되어 있습니다. graph는 callback 또는 resolver-backed semantics가 필요하다는 사실을 기록하며, 이것을 static predicate인 척하지 않습니다.</p>
|
|
1845
|
+
<h3 id="engine-notes-ko-runtime-compiler">Runtime Compiler</h3>
|
|
1846
|
+
<p>compiled guard는 optimized Sea-of-Nodes graph에서 boolean predicate를 방출하고, failed value용 schema-aware diagnostic collector를 함께 생성합니다. runtime <code>is()</code>는 plan-owned schema-specialized kernel을 사용해 recursive node dispatch와 scratch buffer churn을 피합니다. <code>check()</code>는 먼저 plan predicate로 pass/fail verdict를 얻습니다. successful value는 diagnostic collection을 건너뛰고, failed value는 diagnostic collector를 replay해서 path와 issue code를 만듭니다.</p>
|
|
1847
|
+
<p>user-controlled literal, regexp, object key, keyset, dynamic schema, diagnostic name은 generated factory가 capture한 side table에 둡니다. generated source에는 numeric side-table index, fixed helper string, sanitized function name만 들어갑니다.</p>
|
|
1848
|
+
<p>semantic이 local한 scalar node는 direct JavaScript test로 emit됩니다. finite-number check, integer check, string length bound, literal equality, regexp test는 generated hot path에서 helper call 없이 낮아집니다.</p>
|
|
1849
|
+
<p>array와 record IR node는 indexed loop를 emit합니다. static child schema는 optimized graph에서 해당 loop 안으로 inline됩니다. 작은 scalar 또는 union element contract에서 function-call boundary를 피하기 위해서입니다. tuple node는 descriptor-based element access를 보존하고, dynamic edge는 ordinary guard execution과 같은 IR-backed runtime fallback을 사용합니다. 따라서 <code>lazy</code>와 <code>refine</code>의 동작이 유지됩니다.</p>
|
|
1850
|
+
<p>strict object IR은 두 가지 shape으로 emit됩니다. 모든 declared key가 required이면 generated validator는 field descriptor read보다 먼저 strict-key count를 실행합니다. <code>Object.getOwnPropertyNames(value).length</code>를 declared key count와 비교하고, <code>Object.getOwnPropertySymbols(value).length === 0</code>을 요구합니다. V8은 generic <code>Reflect.ownKeys</code> count보다 이 count-only path를 더 잘 최적화하고, obvious extra-key object를 field descriptor를 만지기 전에 거부할 수 있습니다. optional strict object는 missing optional key를 final key count만으로 구분할 수 없으므로 full own-key membership scan을 emit합니다.</p>
|
|
1851
|
+
<p><code>compile(..., { mode: "unsafe" })</code>와 <code>emitAotModule(..., { mode: "unsafe" })</code>는 generated predicate를 trusted-data code shape으로 전환합니다. schema가 <code>undefined</code>를 거부하는 required object field는 descriptor 또는 own-key check 없이 direct <code>value[key]</code> load를 사용합니다. <code>undefined</code>를 허용할 수 있는 required field는 missing required key가 valid <code>undefined</code> value로 collapse되지 않도록 own-key presence guard를 유지합니다. optional field는 present non-<code>undefined</code> value에 direct-load fast path를 쓰고, ambiguous <code>undefined</code> case에서만 own-key check로 fallback합니다.</p>
|
|
1852
|
+
<p>unsafe array, tuple, record, discriminant path도 direct load를 선호합니다. strict object는 own-key array를 할당하는 대신 <code>for...in</code> own-enumerable key loop를 사용합니다. ASCII identifier object key는 <code>value.id</code> 같은 dot-property load로 emit되고, 나머지는 escaped string-literal bracket load로 emit됩니다. 이는 의도적으로 safe mode와 같은 적대적 입력 방어가 아닙니다. getter가 실행될 수 있고, prototype-backed value가 accepted될 수 있으며, symbol 또는 non-enumerable strict extra가 거부되지 않고, static property name이 unsafe generated predicate source에 나타날 수 있습니다.</p>
|
|
1853
|
+
<p><code>mode: "unchecked"</code>는 unsafe direct-read shape을 유지하면서 strict extra-key loop를 제거합니다. caller가 이미 정규화한 object를 위한 trusted-shape path입니다. 이 모드에서 strict object는 더 이상 extra key를 거부하지 않습니다.</p>
|
|
1854
|
+
<p>fast mode는 successful compiled <code>check()</code> result에서 <code>Object.freeze()</code>도 제거합니다. 반환 object는 동일한 <code>{ ok: true, value }</code> shape을 유지하지만 의도적으로 freeze되지 않습니다. failed diagnostic은 success hot path 밖에 있고 reporting을 위해 보존되는 일이 많으므로 계속 freeze됩니다. fast mode의 object diagnostic은 predicate와 같은 direct-read contract에서 생성됩니다. required field는 <code>value.key</code>로 읽고, optional field는 direct load 뒤 <code>undefined</code>일 때 own-key fallback을 사용하며, unsafe strict object는 own enumerable string key를 scan하고, unchecked strict object는 strict-key diagnostic scan을 건너뜁니다. fast mode의 array와 tuple diagnostic은 descriptor probe 대신 direct index로 item을 읽습니다. record diagnostic은 <code>record[key]</code>로 읽습니다. unchecked mode는 inherited enumerable key를 의도적으로 보이게 둡니다. discriminant diagnostic은 tag를 직접 읽고 literal string case를 strict equality로 비교합니다.</p>
|
|
1855
|
+
<h3 id="engine-notes-ko-재귀">재귀</h3>
|
|
1856
|
+
<p>lazy schema는 guard instance마다 getter를 한 번 resolve합니다. 따라서 recursive validation은 stable schema identity를 보고, 반복 validation이 recursive schema graph를 다시 만들지 않습니다.</p>
|
|
1857
|
+
<p>recursive validation은 root-local active pair table을 사용합니다. key는 runtime object identity와 schema identity의 pair입니다. 같은 schema/value pair에 다시 들어오면 그 edge를 short-circuit합니다. 이 방식으로 cyclic object graph도 유한하게 검증하면서, outer frame에서는 원래 object field를 계속 검사합니다.</p>
|
|
1858
|
+
<p>compiled <code>lazy</code>와 <code>refine</code> fallback은 같은 IR-backed runtime path를 사용하므로 recursive behavior가 execution engine 사이에서 일관됩니다.</p>
|
|
1859
|
+
<p><code>checkFirst()</code>는 별도의 generated collector를 사용합니다. 첫 diagnostic이 확정되는 즉시 frozen issue 하나를 반환하며, full <code>check()</code> collector를 끝까지 실행한 뒤 issue array를 자르지 않습니다.</p>
|
|
1860
|
+
<h3 id="engine-notes-ko-json-schema-export">JSON Schema Export</h3>
|
|
1861
|
+
<p>JSON Schema export는 TypeSea schema를 JSON-compatible input value 위에서 의미 손실 없이 표현할 수 있을 때만 성공합니다. runtime-only concept는 typed <code>Result</code> error를 반환합니다.</p>
|
|
1862
|
+
<p>export diagnostic은 모든 것을 parent container로 collapse하지 않고 failed child slot에 path를 유지합니다. 따라서 nested unsupported schema도 schema tree를 수동으로 재구성하지 않고 바로 조치할 수 있습니다.</p>
|
|
1863
|
+
<p>literal check는 runtime-plan과 compiled path 모두에서 <code>Object.is</code>를 사용합니다. diagnostic도 <code>-0</code>을 포함해 같은 literal formatting을 사용하므로 compiled와 runtime-plan <code>check()</code> result가 test에서 byte-for-byte로 비교됩니다.</p>
|
|
1864
|
+
<h3 id="engine-notes-ko-benchmark-범위">Benchmark 범위</h3>
|
|
1865
|
+
<p>benchmark suite는 두 질문을 분리합니다.</p>
|
|
1866
|
+
<ul><li><code>compile.bench.ts</code>는 같은 TypeSea schema를 대상으로 TypeSea runtime-plan validator와 compiled validator를 비교합니다.</li><li><code>ecosystem.bench.ts</code>는 하나의 JSON-compatible strict-object contract를 대상으로 TypeSea runtime-plan, TypeSea compiled, Zod, Valibot, Ajv를 비교합니다.</li></ul>
|
|
1867
|
+
<p>Zod, Valibot, Ajv는 측정용 dev dependency입니다. <code>src</code>에서 import하지 않으며, package policy는 release 전에 runtime, peer, optional, bundled dependency field를 거부합니다.</p>
|
|
1868
|
+
<p>2026-07-04 KST의 마지막 로컬 벤치마크는 JSON-compatible strict-object benchmark에서 아래 ecosystem path를 보고했습니다.</p>
|
|
1869
|
+
<div class="table-wrap"><table><thead><tr><th>Case</th><th>TypeSea runtime plan</th><th>TypeSea compiled safe</th><th>TypeSea compiled unsafe</th><th>TypeSea compiled unchecked</th><th>Ajv compiled</th></tr></thead><tbody><tr><td>Valid <code>is()</code></td><td>513,701 hz</td><td>4,297,306 hz</td><td>36,297,653 hz</td><td>42,581,174 hz</td><td>4,275,389 hz</td></tr>
|
|
1870
|
+
<tr><td>Valid <code>check()</code></td><td>503,232 hz</td><td>3,903,929 hz</td><td>35,568,425 hz</td><td>40,084,605 hz</td><td>4,278,587 hz</td></tr>
|
|
1871
|
+
<tr><td>Invalid <code>is()</code></td><td>3,636,369 hz</td><td>42,080,241 hz</td><td>49,654,076 hz</td><td>50,482,732 hz</td><td>27,820,643 hz</td></tr>
|
|
1872
|
+
<tr><td>Invalid <code>check()</code></td><td>420,446 hz</td><td>2,086,129 hz</td><td>3,077,367 hz</td><td>3,673,508 hz</td><td>28,713,035 hz</td></tr></tbody></table></div>
|
|
1873
|
+
<p>benchmark number는 machine-local telemetry입니다. regression을 잡는 데 유용하지만 고정된 throughput floor를 약속하지 않습니다. unsafe와 unchecked number는 safe mode와 hostile-input equivalent가 아닙니다.</p>
|
|
1874
|
+
</div>
|
|
1875
|
+
</article>
|
|
1173
1876
|
</main>
|
|
1174
1877
|
</div>
|
|
1175
1878
|
</div>
|
|
1176
|
-
|
|
1177
|
-
<script>
|
|
1178
|
-
const search = document.querySelector("[data-search]");
|
|
1179
|
-
const sections = Array.from(document.querySelectorAll("[data-doc-section]"));
|
|
1180
|
-
const links = Array.from(document.querySelectorAll(".nav a"));
|
|
1181
|
-
const groups = Array.from(document.querySelectorAll(".nav-group"));
|
|
1182
|
-
|
|
1183
|
-
if (search instanceof HTMLInputElement) {
|
|
1184
|
-
search.addEventListener("input", () => {
|
|
1185
|
-
const query = search.value.trim().toLowerCase();
|
|
1186
|
-
for (const section of sections) {
|
|
1187
|
-
const title = section.getAttribute("data-title") ?? "";
|
|
1188
|
-
const text = section.textContent?.toLowerCase() ?? "";
|
|
1189
|
-
const visible = query === "" || title.includes(query) || text.includes(query);
|
|
1190
|
-
section.classList.toggle("hidden", !visible);
|
|
1191
|
-
}
|
|
1192
|
-
for (const link of links) {
|
|
1193
|
-
const target = link.getAttribute("href") ?? "";
|
|
1194
|
-
const section = target.startsWith("#")
|
|
1195
|
-
? document.getElementById(target.slice(1))
|
|
1196
|
-
: null;
|
|
1197
|
-
const visible = section === null || !section.classList.contains("hidden");
|
|
1198
|
-
link.classList.toggle("hidden", !visible);
|
|
1199
|
-
}
|
|
1200
|
-
for (const group of groups) {
|
|
1201
|
-
let sibling = group.nextElementSibling;
|
|
1202
|
-
let anyVisible = false;
|
|
1203
|
-
while (sibling !== null && !sibling.classList.contains("nav-group")) {
|
|
1204
|
-
if (sibling.matches("a") && !sibling.classList.contains("hidden")) {
|
|
1205
|
-
anyVisible = true;
|
|
1206
|
-
}
|
|
1207
|
-
sibling = sibling.nextElementSibling;
|
|
1208
|
-
}
|
|
1209
|
-
group.classList.toggle("hidden", !anyVisible);
|
|
1210
|
-
}
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
const linkById = new Map(
|
|
1215
|
-
links
|
|
1216
|
-
.map((link) => [link.getAttribute("href")?.slice(1) ?? "", link])
|
|
1217
|
-
.filter(([id]) => id !== "")
|
|
1218
|
-
);
|
|
1219
|
-
|
|
1220
|
-
if ("IntersectionObserver" in window) {
|
|
1221
|
-
const observer = new IntersectionObserver(
|
|
1222
|
-
(entries) => {
|
|
1223
|
-
for (const entry of entries) {
|
|
1224
|
-
if (entry.isIntersecting) {
|
|
1225
|
-
for (const link of links) {
|
|
1226
|
-
link.classList.toggle(
|
|
1227
|
-
"active",
|
|
1228
|
-
link === linkById.get(entry.target.id)
|
|
1229
|
-
);
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
},
|
|
1234
|
-
{ rootMargin: "-20% 0px -70% 0px" }
|
|
1235
|
-
);
|
|
1236
|
-
for (const section of sections) {
|
|
1237
|
-
observer.observe(section);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
</script>
|
|
1241
1879
|
</body>
|
|
1242
1880
|
</html>
|