runtypex 0.2.3 → 0.2.4

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.
@@ -0,0 +1,292 @@
1
+
2
+ # 런타임 검증
3
+
4
+ `runtypex`의 런타임 검증은 TypeScript 타입을 기반으로 런타임 가드 함수를 생성하는 기능입니다.
5
+
6
+ TypeScript 타입은 일반적으로 컴파일 후 JavaScript 런타임에 남지 않습니다. `runtypex`는 빌드 중 TypeScript 타입 체커를 사용해 타입 정보를 읽고, 이를 일반 JavaScript 검증 코드로 변환합니다.
7
+
8
+ 즉, 다음과 같은 타입을:
9
+
10
+ ```ts
11
+ interface User {
12
+ id: number;
13
+ name: string;
14
+ }
15
+ ```
16
+
17
+ 런타임에서 사용할 수 있는 검증 함수로 바꿀 수 있습니다.
18
+
19
+ ```ts
20
+ const isUser = (value) =>
21
+ typeof value === "object" &&
22
+ value !== null &&
23
+ typeof value.id === "number" &&
24
+ typeof value.name === "string";
25
+ ```
26
+
27
+ <br/>
28
+
29
+ ## API
30
+
31
+ `runtypex`는 런타임 검증을 위해 두 가지 API를 제공합니다.
32
+
33
+ ```ts
34
+ import { makeAssert, makeValidate } from "runtypex";
35
+ ```
36
+
37
+ | API | 반환값 | 용도 |
38
+ | ------------------- | ------------ | ------------------------------ |
39
+ | `makeValidate<T>()` | 판별 함수 | 값이 타입 `T`인지 `boolean`으로 확인합니다. |
40
+ | `makeAssert<T>()` | assertion 함수 | 값이 타입 `T`가 아니면 예외를 던집니다. |
41
+
42
+ <br/>
43
+
44
+ ## `makeValidate<T>()`
45
+
46
+ `makeValidate<T>()`는 타입 판별 함수를 반환합니다.
47
+
48
+ ```ts
49
+ const isUser = makeValidate<User>();
50
+
51
+ if (isUser(input)) {
52
+ input.id;
53
+ }
54
+ ```
55
+
56
+ `isUser(input)`이 `true`를 반환하면 TypeScript는 `input`을 `User` 타입으로 좁혀서 다룰 수 있습니다.
57
+
58
+ 예를 들어 다음 타입이 있다고 가정합니다.
59
+
60
+ ```ts
61
+ interface User {
62
+ id: number;
63
+ name: string;
64
+ }
65
+ ```
66
+
67
+ 다음 코드는:
68
+
69
+ ```ts
70
+ const isUser = makeValidate<User>();
71
+ ```
72
+
73
+ 트랜스포머가 활성화된 빌드에서는 대략 다음과 같은 함수로 변환됩니다.
74
+
75
+ ```ts
76
+ const isUser = (input) =>
77
+ typeof input === "object" &&
78
+ input !== null &&
79
+ typeof input.id === "number" &&
80
+ typeof input.name === "string";
81
+ ```
82
+
83
+ 생성된 코드는 런타임 리플렉션에 의존하지 않습니다. 일반 JavaScript 조건문으로 동작합니다.
84
+
85
+ <br/>
86
+
87
+ ## `makeAssert<T>()`
88
+
89
+ `makeAssert<T>()`는 assertion 함수를 반환합니다.
90
+
91
+ ```ts
92
+ const assertUser = makeAssert<User>();
93
+
94
+ assertUser(input);
95
+ input.id;
96
+ ```
97
+
98
+ `assertUser(input)`이 정상적으로 통과하면, 이후 코드에서 `input`을 `User` 타입으로 다룰 수 있습니다.
99
+
100
+ 값이 `User` 타입에 맞지 않으면 `TypeError`를 던집니다.
101
+
102
+ ```ts
103
+ const assertUser = makeAssert<User>();
104
+
105
+ assertUser(input); // 실패하면 TypeError
106
+ ```
107
+
108
+ 트랜스포머가 활성화된 경우 다음과 같은 형태의 코드로 변환됩니다.
109
+
110
+ ```ts
111
+ const assertUser = (function () {
112
+ const G = (input) =>
113
+ typeof input === "object" &&
114
+ input !== null &&
115
+ typeof input.id === "number" &&
116
+ typeof input.name === "string";
117
+
118
+ return (input) => {
119
+ if (!G(input)) {
120
+ throw new TypeError("[runtypex] Validation failed.");
121
+ }
122
+ };
123
+ })();
124
+ ```
125
+
126
+ <br/>
127
+
128
+ ## 동작 방식
129
+
130
+ 다음과 같이 작성하면:
131
+
132
+ ```ts
133
+ interface User {
134
+ id: number;
135
+ name: string;
136
+ }
137
+
138
+ const isUser = makeValidate<User>();
139
+ ```
140
+
141
+ `runtypex` 트랜스포머는 빌드 중 TypeScript 타입 체커를 통해 `User` 타입을 읽습니다.
142
+
143
+ 그런 다음 `makeValidate<User>()` 호출을 생성된 JavaScript 검증 함수로 대체합니다.
144
+
145
+ ```js
146
+ const isUser = (v) =>
147
+ typeof v === "object" &&
148
+ v !== null &&
149
+ typeof v.id === "number" &&
150
+ typeof v.name === "string";
151
+ ```
152
+
153
+ 이 과정에서 별도의 스키마 파일이나 런타임 타입 메타데이터는 필요하지 않습니다.
154
+
155
+ 생성된 검증 함수는 `makeValidate<User>()` 호출이 있던 파일 안에 인라인됩니다.
156
+
157
+ <br/>
158
+
159
+ ## 빌드 시점 변환과 런타임 폴백
160
+
161
+ `makeValidate<T>()`와 `makeAssert<T>()`는 트랜스포머가 실행될 때 가장 의미가 있습니다.
162
+
163
+ TypeScript 타입 `T`는 JavaScript 런타임에 존재하지 않습니다. 따라서 트랜스포머 없이 순수 런타임만으로는 `T`의 구조를 알 수 없습니다.
164
+
165
+ <br/>
166
+
167
+ ## 트랜스포머가 활성화된 경우
168
+
169
+ Vite 플러그인이나 TypeScript 트랜스포머가 활성화되어 있으면 다음 호출은:
170
+
171
+ ```ts
172
+ const isUser = makeValidate<User>();
173
+ ```
174
+
175
+ 변환된 소스 파일 안에서 실제 검증 함수로 대체됩니다.
176
+
177
+ ```ts
178
+ const isUser = (input) =>
179
+ typeof input === "object" &&
180
+ input !== null &&
181
+ typeof input.id === "number" &&
182
+ typeof input.name === "string";
183
+ ```
184
+
185
+ 이때 별도의 스키마 파일은 생성되지 않습니다.
186
+
187
+ 검증 함수는 기존 소스 파일 안에 인라인됩니다.
188
+
189
+ <br/>
190
+
191
+ ## 트랜스포머가 없는 경우
192
+
193
+ 트랜스포머가 실행되지 않으면 패키지의 런타임 폴백이 사용됩니다.
194
+
195
+ ```ts
196
+ function __validate<T>(_value: unknown): boolean {
197
+ return true;
198
+ }
199
+
200
+ export function makeValidate<T>() {
201
+ return (value: unknown): value is T => __validate<T>(value);
202
+ }
203
+ ```
204
+
205
+ 이 폴백은 의도적으로 자리표시자 역할만 합니다.
206
+
207
+ 중요한 점은, 이 경우 `T`의 구조를 실제로 검증하지 않는다는 것입니다.
208
+
209
+ ```ts
210
+ const isUser = makeValidate<User>();
211
+
212
+ isUser({}); // 트랜스포머가 없으면 실제 구조 검증을 하지 않습니다.
213
+ ```
214
+
215
+ 따라서 실제 런타임 검증이 필요하다면 반드시 Vite 플러그인 또는 TypeScript 트랜스포머를 빌드에 연동해야 합니다.
216
+
217
+ <br/>
218
+
219
+ ## `makeAssert<T>()`의 폴백 동작
220
+
221
+ 트랜스포머가 없으면 `makeAssert<T>()`는 내부적으로 `makeValidate<T>()`를 사용합니다.
222
+
223
+ ```ts
224
+ const assertUser = makeAssert<User>();
225
+ ```
226
+
227
+ 하지만 트랜스포머가 없는 환경에서는 `makeValidate<T>()` 역시 실제 타입 구조를 검사하지 않습니다.
228
+
229
+ 따라서 `makeAssert<T>()`도 같은 자리표시자 폴백을 사용하며, `T`를 실제로 검증하지 않습니다.
230
+
231
+ 정리하면 다음과 같습니다.
232
+
233
+ | 환경 | `makeValidate<T>()` | `makeAssert<T>()` |
234
+ | -------- | ------------------- | ------------------------- |
235
+ | 트랜스포머 있음 | 실제 타입 검증 함수로 변환됩니다. | 실제 타입 검증 후 실패 시 예외를 던집니다. |
236
+ | 트랜스포머 없음 | 자리표시자 폴백을 사용합니다. | 같은 자리표시자 폴백을 사용합니다. |
237
+
238
+ <br/>
239
+
240
+ ## 프로덕션에서 검증 제거하기
241
+
242
+ 트랜스포머 옵션에서 `removeInProd: true`를 활성화하고, `NODE_ENV`가 `production`이면 생성된 검증기는 no-op에 해당하는 함수로 대체됩니다.
243
+
244
+ ```ts
245
+ makeValidate<T>(); // (_) => true
246
+ makeAssert<T>(); // (_) => {}
247
+ ```
248
+
249
+ 즉, 프로덕션 빌드에서는 검증 비용을 제거할 수 있습니다.
250
+
251
+ 이 옵션은 런타임 검증을 **프로덕션 경계의 보안 장치**가 아니라 **개발 중 안전장치**로 사용할 때 적합합니다.
252
+
253
+ 예를 들어 개발 환경에서는 DTO 구조가 예상과 다른지 빠르게 확인하고, 프로덕션에서는 검증 비용을 줄이고 싶을 때 사용할 수 있습니다.
254
+
255
+ 반대로 외부 입력을 프로덕션에서도 반드시 검증해야 한다면 `removeInProd: true`를 사용하지 않는 것이 좋습니다.
256
+
257
+ <br/>
258
+
259
+ ## 지원하는 타입 형태
260
+
261
+ 현재 emitter는 일반적으로 사용하는 TypeScript 타입 형태를 지원합니다.
262
+
263
+ | 타입 형태 | 예시 | |
264
+ | --------------------------- | ------------------------------------------------- | ----------- |
265
+ | primitive | `string`, `number`, `boolean` | |
266
+ | object / interface property | `{ id: string }`, `interface User { id: string }` | |
267
+ | optional property | `{ name?: string }` | |
268
+ | array | `string[]`, `Array<User>` | |
269
+ | tuple | `[string, number]` | |
270
+ | union | `"ACTIVE" | "INACTIVE"` |
271
+ | intersection | `A & B` | |
272
+ | literal type | `"admin"`, `1`, `true` | |
273
+ | enum | `enum Status { Active }` | |
274
+
275
+ 엣지 케이스별 정확한 동작은 emitter 테스트를 기준으로 확인하는 것이 좋습니다.
276
+
277
+ <br/>
278
+
279
+ ## 요약
280
+
281
+ `runtypex`의 런타임 검증은 TypeScript 타입을 빌드 시점에 JavaScript 검증 함수로 변환합니다.
282
+
283
+ | 항목 | 설명 |
284
+ | -------------------- | ---------------------------------------- |
285
+ | `makeValidate<T>()` | 값이 `T`인지 확인하는 판별 함수를 생성합니다. |
286
+ | `makeAssert<T>()` | 값이 `T`가 아니면 예외를 던지는 assertion 함수를 생성합니다. |
287
+ | 트랜스포머 있음 | TypeScript 타입을 기반으로 실제 검증 코드가 인라인됩니다. |
288
+ | 트랜스포머 없음 | 타입 정보를 알 수 없어 자리표시자 폴백이 사용됩니다. |
289
+ | `removeInProd: true` | 프로덕션에서 검증 함수를 no-op으로 대체합니다. |
290
+ | 생성 방식 | 별도 스키마 파일 없이 기존 소스 파일 안에 검증 함수가 인라인됩니다. |
291
+
292
+ 실제 타입 검증을 사용하려면 `makeValidate<T>()` 또는 `makeAssert<T>()`만 호출하는 것으로는 충분하지 않습니다. 반드시 Vite 플러그인이나 TypeScript 트랜스포머를 빌드에 연동해야 합니다.
package/docs/mapper.md CHANGED
@@ -56,6 +56,24 @@ For each domain key, the mapper:
56
56
  3. runs the transform callback when one is provided
57
57
  4. writes the result to the domain output object
58
58
 
59
+ The runtime fallback keeps the mapping behavior available without a build
60
+ integration:
61
+
62
+ ```ts
63
+ const toUser = makeMapper<UserDto, User>(userMap);
64
+
65
+ toUser({
66
+ user_id: "u1",
67
+ profile: { name: "Lux" },
68
+ status: "ACTIVE",
69
+ });
70
+ ```
71
+
72
+ At runtime, `makeMapper()` walks the `userMap` object, reads each `from` path
73
+ from the input DTO, applies `default` and `transform` when present, and returns
74
+ the domain object. It does not create a new source file and it does not inline
75
+ generated code.
76
+
59
77
  ## Transformer Behavior
60
78
 
61
79
  With the transformer enabled, this call:
@@ -67,6 +85,75 @@ const toUser = makeMapper<UserDto, User>(userMap);
67
85
  is replaced with an inline mapper function. The generated function can validate
68
86
  the DTO input before mapping and validate the domain output after mapping.
69
87
 
88
+ The transformed source contains code shaped like this:
89
+
90
+ ```ts
91
+ const toUser = (function () {
92
+ const S = {
93
+ id: { from: "user_id" },
94
+ displayName: { from: "profile.name" },
95
+ isActive: {
96
+ from: "status",
97
+ transform: (value) => value === "ACTIVE",
98
+ },
99
+ };
100
+
101
+ const VD = (input) =>
102
+ typeof input === "object" &&
103
+ input !== null &&
104
+ typeof input.user_id === "string" &&
105
+ typeof input.profile === "object" &&
106
+ input.profile !== null &&
107
+ typeof input.profile.name === "string" &&
108
+ (input.status === "ACTIVE" || input.status === "INACTIVE");
109
+
110
+ const VO = (input) =>
111
+ typeof input === "object" &&
112
+ input !== null &&
113
+ typeof input.id === "string" &&
114
+ typeof input.displayName === "string" &&
115
+ typeof input.isActive === "boolean";
116
+
117
+ return (input) => {
118
+ if (!VD(input)) throw new TypeError("[runtypex] DTO validation failed.");
119
+
120
+ const R = (key, raw) => {
121
+ const rule = S[key];
122
+ const value =
123
+ raw === undefined && Object.prototype.hasOwnProperty.call(rule, "default")
124
+ ? rule.default
125
+ : raw;
126
+ return typeof rule.transform === "function" ? rule.transform(value, input) : value;
127
+ };
128
+
129
+ const output = {
130
+ id: R("id", input["user_id"]),
131
+ displayName: R("displayName", input["profile"]["name"]),
132
+ isActive: R("isActive", input["status"]),
133
+ };
134
+
135
+ if (!VO(output)) throw new TypeError("[runtypex] Domain validation failed.");
136
+ return output;
137
+ };
138
+ })();
139
+ ```
140
+
141
+ No separate mapper file is created by the transformer. The mapper function is
142
+ inlined into the transformed file that contained `makeMapper<TDto, TDomain>()`.
143
+
144
+ When `removeInProd: true` is enabled and `NODE_ENV` is `production`, the mapper
145
+ itself is still generated, but DTO and domain validation guards are omitted:
146
+
147
+ ```ts
148
+ const output = {
149
+ id: R("id", input["user_id"]),
150
+ displayName: R("displayName", input["profile"]["name"]),
151
+ isActive: R("isActive", input["status"]),
152
+ };
153
+
154
+ return output;
155
+ ```
156
+
70
157
  This gives you one mapping declaration while still allowing build-time optimized
71
158
  runtime code.
72
159
 
@@ -54,6 +54,77 @@ const isUser = (v) =>
54
54
 
55
55
  No runtime reflection is required. The generated code is plain JavaScript.
56
56
 
57
+ ## Build-Time vs Runtime Fallback
58
+
59
+ `makeValidate<T>()` and `makeAssert<T>()` are useful only as build-time markers
60
+ unless the transformer runs. The TypeScript type `T` is erased at runtime, so the
61
+ plain runtime fallback cannot inspect it.
62
+
63
+ ### `makeValidate<T>()`
64
+
65
+ You write:
66
+
67
+ ```ts
68
+ const isUser = makeValidate<User>();
69
+ ```
70
+
71
+ With the Vite plugin or TypeScript transformer enabled, the call is replaced in
72
+ the transformed source file with a generated predicate:
73
+
74
+ ```ts
75
+ const isUser = (input) =>
76
+ typeof input === "object" &&
77
+ input !== null &&
78
+ typeof input.id === "number" &&
79
+ typeof input.name === "string";
80
+ ```
81
+
82
+ No extra schema file is created. The generated function is inlined into the file
83
+ that contained `makeValidate<User>()`.
84
+
85
+ Without the transformer, the package runtime fallback is used:
86
+
87
+ ```ts
88
+ function __validate<T>(_value: unknown): boolean {
89
+ return true;
90
+ }
91
+
92
+ export function makeValidate<T>() {
93
+ return (value: unknown): value is T => __validate<T>(value);
94
+ }
95
+ ```
96
+
97
+ That fallback is intentionally only a placeholder. It does not validate the
98
+ shape of `T`.
99
+
100
+ ### `makeAssert<T>()`
101
+
102
+ You write:
103
+
104
+ ```ts
105
+ const assertUser = makeAssert<User>();
106
+ ```
107
+
108
+ With the transformer enabled, the call is replaced with an assertion function
109
+ that closes over the generated predicate:
110
+
111
+ ```ts
112
+ const assertUser = (function () {
113
+ const G = (input) =>
114
+ typeof input === "object" &&
115
+ input !== null &&
116
+ typeof input.id === "number" &&
117
+ typeof input.name === "string";
118
+
119
+ return (input) => {
120
+ if (!G(input)) throw new TypeError("[runtypex] Validation failed.");
121
+ };
122
+ })();
123
+ ```
124
+
125
+ Without the transformer, `makeAssert<T>()` calls `makeValidate<T>()`, so it uses
126
+ the same placeholder fallback and does not validate `T`.
127
+
57
128
  ## Production Removal
58
129
 
59
130
  When `removeInProd: true` is enabled and `NODE_ENV` is `production`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runtypex",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Runtime type guards compiled from TypeScript types.",
5
5
  "license": "MIT",
6
6
  "author": "KumJungMin",