runtypex 0.1.13 → 0.2.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.
Files changed (72) hide show
  1. package/README.md +112 -278
  2. package/dist/cjs/core/emitArrayOrTuple.d.ts +6 -0
  3. package/dist/cjs/core/emitArrayOrTuple.js +40 -0
  4. package/dist/cjs/core/emitLiteralOrEnum.d.ts +4 -0
  5. package/dist/cjs/core/emitLiteralOrEnum.js +47 -0
  6. package/dist/cjs/core/emitMapperFromSpec.d.ts +32 -0
  7. package/dist/cjs/core/emitMapperFromSpec.js +199 -0
  8. package/dist/cjs/core/emitObject.d.ts +6 -0
  9. package/dist/cjs/core/emitObject.js +30 -0
  10. package/dist/cjs/core/emitPrimitive.d.ts +6 -0
  11. package/dist/cjs/core/emitPrimitive.js +29 -0
  12. package/dist/cjs/core/emitUnionOrIntersection.d.ts +6 -0
  13. package/dist/cjs/core/emitUnionOrIntersection.js +15 -0
  14. package/dist/cjs/core/index.d.ts +17 -0
  15. package/dist/cjs/core/index.js +41 -0
  16. package/dist/cjs/core/path.d.ts +9 -0
  17. package/dist/cjs/core/path.js +42 -0
  18. package/dist/cjs/generator/generate-jsdoc.d.ts +13 -0
  19. package/dist/cjs/generator/generate-jsdoc.js +69 -0
  20. package/dist/cjs/generator/index.d.ts +1 -0
  21. package/dist/cjs/generator/index.js +17 -0
  22. package/dist/cjs/index.d.ts +4 -0
  23. package/dist/cjs/index.js +20 -0
  24. package/dist/cjs/mapper/index.d.ts +1 -0
  25. package/dist/cjs/mapper/index.js +17 -0
  26. package/dist/cjs/runtime/index.d.ts +2 -0
  27. package/dist/cjs/runtime/index.js +18 -0
  28. package/dist/cjs/runtime/mapper.d.ts +71 -0
  29. package/dist/cjs/runtime/mapper.js +79 -0
  30. package/dist/cjs/runtime/validate.d.ts +11 -0
  31. package/dist/cjs/runtime/validate.js +18 -0
  32. package/dist/cjs/transformer/helper.d.ts +2 -0
  33. package/dist/cjs/transformer/helper.js +63 -0
  34. package/dist/cjs/transformer/index.d.ts +3 -0
  35. package/dist/cjs/transformer/index.js +12 -0
  36. package/dist/cjs/transformer/ts-transformer.d.ts +29 -0
  37. package/dist/cjs/transformer/ts-transformer.js +109 -0
  38. package/dist/cjs/transformer/vite-plugin.d.ts +18 -0
  39. package/dist/cjs/transformer/vite-plugin.js +72 -0
  40. package/dist/esm/core/emitArrayOrTuple.d.ts +1 -1
  41. package/dist/esm/core/emitLiteralOrEnum.d.ts +1 -1
  42. package/dist/esm/core/emitMapperFromSpec.d.ts +32 -0
  43. package/dist/esm/core/emitMapperFromSpec.js +189 -0
  44. package/dist/esm/core/emitObject.d.ts +1 -1
  45. package/dist/esm/core/emitObject.js +4 -2
  46. package/dist/esm/core/emitPrimitive.d.ts +1 -1
  47. package/dist/esm/core/emitUnionOrIntersection.d.ts +1 -1
  48. package/dist/esm/core/path.d.ts +9 -0
  49. package/dist/esm/core/path.js +36 -0
  50. package/dist/esm/generator/generate-jsdoc.d.ts +13 -0
  51. package/dist/esm/generator/generate-jsdoc.js +63 -0
  52. package/dist/esm/generator/index.d.ts +1 -0
  53. package/dist/esm/generator/index.js +1 -0
  54. package/dist/esm/index.d.ts +4 -3
  55. package/dist/esm/index.js +1 -0
  56. package/dist/esm/mapper/index.d.ts +1 -0
  57. package/dist/esm/mapper/index.js +1 -0
  58. package/dist/esm/runtime/index.d.ts +2 -0
  59. package/dist/esm/runtime/index.js +2 -0
  60. package/dist/esm/runtime/mapper.d.ts +71 -0
  61. package/dist/esm/runtime/mapper.js +71 -0
  62. package/dist/esm/transformer/index.d.ts +3 -0
  63. package/dist/esm/transformer/index.js +3 -0
  64. package/dist/esm/transformer/ts-transformer.d.ts +8 -4
  65. package/dist/esm/transformer/ts-transformer.js +51 -10
  66. package/dist/esm/transformer/vite-plugin.js +7 -32
  67. package/docs/build-integrations.md +89 -0
  68. package/docs/jsdoc-generation.md +88 -0
  69. package/docs/mapper.md +104 -0
  70. package/docs/mapping-policy.md +78 -0
  71. package/docs/runtime-validation.md +84 -0
  72. package/package.json +76 -36
@@ -1,6 +1,6 @@
1
1
  import ts from "typescript";
2
2
  import { emitGuardFromType } from "../core/index.js";
3
- import { resolveTypeByName } from "./helper.js";
3
+ import { emitMapperFromSpec } from "../core/emitMapperFromSpec.js";
4
4
  /**
5
5
  * 🧩 tsTransformer
6
6
  * TypeScript custom transformer (BEFORE) factory.
@@ -22,7 +22,6 @@ import { resolveTypeByName } from "./helper.js";
22
22
  * ✅ Optionally removed in production builds
23
23
  */
24
24
  export default function tsTransformer(options) {
25
- const { program } = options;
26
25
  const checker = options.program.getTypeChecker();
27
26
  const removeInProd = !!options.removeInProd;
28
27
  const prod = process.env.NODE_ENV === "production";
@@ -30,13 +29,8 @@ export default function tsTransformer(options) {
30
29
  const visit = (node) => {
31
30
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
32
31
  const name = node.expression.text;
33
- const targetFunctions = ["makeValidate", "makeAssert"];
34
- if (targetFunctions.includes(name) && node.typeArguments?.length) {
35
- const typeNode = node.typeArguments[0];
36
- const typeName = typeNode.getText();
37
- const type = resolveTypeByName(program, node.getSourceFile(), checker, typeName);
38
- if (!type)
39
- return node;
32
+ if ((name === "makeValidate" || name === "makeAssert") && node.typeArguments?.length) {
33
+ const type = checker.getTypeFromTypeNode(node.typeArguments[0]);
40
34
  const isRemovedInProd = removeInProd && prod;
41
35
  switch (name) {
42
36
  case "makeValidate":
@@ -45,18 +39,65 @@ export default function tsTransformer(options) {
45
39
  return _emitMakeAssert(checker, type, isRemovedInProd);
46
40
  }
47
41
  }
42
+ // makeMapper<TDto, TDomain>(spec) becomes an inline validating mapper.
43
+ if (name === "makeMapper" && node.typeArguments?.length === 2 && node.arguments[0]) {
44
+ const mapperCallOptions = _readMapperCallOptions(node.arguments[1]);
45
+ const mapper = emitMapperFromSpec({
46
+ checker,
47
+ dtoType: checker.getTypeFromTypeNode(node.typeArguments[0]),
48
+ domainType: checker.getTypeFromTypeNode(node.typeArguments[1]),
49
+ specNode: node.arguments[0],
50
+ sourceFile: node.getSourceFile(),
51
+ options: {
52
+ validateDto: !(removeInProd && prod) && options.validateDto !== false,
53
+ validateDomain: !(removeInProd && prod) && options.validateDomain !== false,
54
+ mappingPolicy: mapperCallOptions.policy,
55
+ policyMode: mapperCallOptions.policyMode,
56
+ },
57
+ });
58
+ if (mapper)
59
+ return ts.factory.createIdentifier(mapper);
60
+ }
48
61
  }
49
62
  return ts.visitEachChild(node, visit, context);
50
63
  };
51
64
  return (sf) => ts.visitNode(sf, visit);
52
65
  };
53
66
  }
67
+ function _readMapperCallOptions(node) {
68
+ if (!node)
69
+ return {};
70
+ const expr = ts.isAsExpression(node) || ts.isParenthesizedExpression(node) ? node.expression : node;
71
+ if (!ts.isObjectLiteralExpression(expr))
72
+ return {};
73
+ return {
74
+ policy: _readExpressionProperty(expr, "policy"),
75
+ policyMode: _readPolicyMode(expr),
76
+ };
77
+ }
78
+ function _readExpressionProperty(object, name) {
79
+ for (const item of object.properties) {
80
+ if (ts.isPropertyAssignment(item) && ts.isIdentifier(item.name) && item.name.text === name) {
81
+ return item.initializer;
82
+ }
83
+ if (ts.isShorthandPropertyAssignment(item) && item.name.text === name) {
84
+ return item.name;
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+ function _readPolicyMode(object) {
90
+ const mode = _readExpressionProperty(object, "policyMode");
91
+ return mode && ts.isStringLiteral(mode) && (mode.text === "warn" || mode.text === "error") ? mode.text : undefined;
92
+ }
54
93
  function _emitMakeValidate(checker, type, isRemovedInProd) {
55
94
  const guard = isRemovedInProd ? "((_)=>true)" : emitGuardFromType(checker, type);
56
95
  return ts.factory.createIdentifier(guard);
57
96
  }
58
97
  function _emitMakeAssert(checker, type, isRemovedInProd) {
59
- const guard = isRemovedInProd ? "((_)=>{})" : emitGuardFromType(checker, type);
98
+ if (isRemovedInProd)
99
+ return ts.factory.createIdentifier("((_)=>{})");
100
+ const guard = emitGuardFromType(checker, type);
60
101
  const txt = `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
61
102
  return ts.factory.createIdentifier(txt);
62
103
  }
@@ -1,7 +1,6 @@
1
1
  import ts from "typescript";
2
2
  import path from "node:path";
3
- import { emitGuardFromType } from "../core/index.js";
4
- import { resolveTypeByName } from "./helper.js";
3
+ import tsTransformer from "./ts-transformer.js";
5
4
  /**
6
5
  * 🧩 vitePluginRuntypex
7
6
  * A Vite plugin that performs build-time type → runtime validation transformation.
@@ -18,24 +17,21 @@ import { resolveTypeByName } from "./helper.js";
18
17
  */
19
18
  export default function vitePluginRuntypex(options) {
20
19
  const removeInProd = !!options?.removeInProd;
21
- const prod = process.env.NODE_ENV === "production";
22
20
  return {
23
21
  name: "vite-plugin-runtypex",
24
22
  enforce: "pre",
25
23
  transform(code, id) {
26
24
  const isTS = id.endsWith(".ts") || id.endsWith(".tsx");
27
- const isTargetFunction = /make(?:Validate|Assert)</.test(code);
25
+ const isTargetFunction = /make(?:Validate|Assert|Mapper)</.test(code);
28
26
  if (!isTS || !isTargetFunction)
29
27
  return;
30
- const { program, checker } = _createProgramFor(id);
28
+ const { program } = _createProgramFor(id);
31
29
  const sf = program.getSourceFile(id);
32
30
  if (!sf)
33
31
  return;
34
- let mutated = code;
35
- // makeAssert<T>()
36
- mutated = mutated.replace(/makeAssert<\s*([^>]+)\s*>\s*\(\s*\)/g, (_m, typeName) => _emitMakeAssert({ program, checker, sf, typeName, prod, removeInProd }) ?? _m);
37
- // ② makeValidate<T>()
38
- mutated = mutated.replace(/makeValidate<\s*([^>]+)\s*>\s*\(\s*\)/g, (_m, typeName) => _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd }) ?? _m);
32
+ const result = ts.transform(sf, [tsTransformer({ program, removeInProd })]);
33
+ const mutated = ts.createPrinter().printFile(result.transformed[0]);
34
+ result.dispose();
39
35
  return mutated === code ? null : { code: mutated, map: null };
40
36
  },
41
37
  };
@@ -50,8 +46,7 @@ function _createProgramFor(file) {
50
46
  throw new Error(ts.flattenDiagnosticMessageText(cfg.error.messageText, "\n"));
51
47
  const parsed = ts.parseJsonConfigFileContent(cfg.config, ts.sys, path.dirname(tsconfig));
52
48
  const program = ts.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
53
- const checker = program.getTypeChecker();
54
- return { program, checker };
49
+ return { program };
55
50
  }
56
51
  function _findNearestTsconfig(start) {
57
52
  let dir = start;
@@ -69,23 +64,3 @@ function _findNearestTsconfig(start) {
69
64
  return fallback;
70
65
  throw new Error("tsconfig.json not found");
71
66
  }
72
- // ──────────────────────────────────────────────
73
- // ② Emit Helpers
74
- // ──────────────────────────────────────────────
75
- function _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd, }) {
76
- if (removeInProd && prod)
77
- return `((_)=>true)`;
78
- const type = resolveTypeByName(program, sf, checker, typeName.trim());
79
- if (!type)
80
- return null;
81
- return emitGuardFromType(checker, type);
82
- }
83
- function _emitMakeAssert({ program, checker, sf, typeName, prod, removeInProd, }) {
84
- if (removeInProd && prod)
85
- return `((_)=>{})`;
86
- const type = resolveTypeByName(program, sf, checker, typeName.trim());
87
- if (!type)
88
- return null;
89
- const guard = emitGuardFromType(checker, type);
90
- return `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
91
- }
@@ -0,0 +1,89 @@
1
+ # Build Integrations
2
+
3
+ `runtypex` relies on the TypeScript compiler API, so its full value comes from
4
+ running the transformer during build.
5
+
6
+ ## Vite
7
+
8
+ ```ts
9
+ // vite.config.ts
10
+ import { defineConfig } from "vite";
11
+ import { vitePlugin as runtypex } from "runtypex";
12
+
13
+ export default defineConfig({
14
+ plugins: [runtypex()],
15
+ });
16
+ ```
17
+
18
+ The Vite plugin scans TypeScript files for:
19
+
20
+ - `makeValidate<T>()`
21
+ - `makeAssert<T>()`
22
+ - `makeMapper<TDto, TDomain>()`
23
+
24
+ When a target call is found, the plugin creates a TypeScript program for the
25
+ nearest `tsconfig.json`, runs the transformer, and returns the transformed code
26
+ to Vite.
27
+
28
+ ## ts-loader
29
+
30
+ ```js
31
+ // webpack.config.js
32
+ const { tsTransformer } = require("runtypex");
33
+
34
+ module.exports = {
35
+ module: {
36
+ rules: [
37
+ {
38
+ test: /\.tsx?$/,
39
+ loader: "ts-loader",
40
+ options: {
41
+ getCustomTransformers: (program) => ({
42
+ before: [tsTransformer({ program })],
43
+ }),
44
+ },
45
+ },
46
+ ],
47
+ },
48
+ };
49
+ ```
50
+
51
+ ## Transformer Options
52
+
53
+ ```ts
54
+ tsTransformer({
55
+ program,
56
+ removeInProd: true,
57
+ validateDto: true,
58
+ validateDomain: true,
59
+ });
60
+ ```
61
+
62
+ | Option | Default | Description |
63
+ | --- | --- | --- |
64
+ | `program` | required | TypeScript program used to resolve types. |
65
+ | `removeInProd` | `false` | Replaces generated validation with no-op functions in production. |
66
+ | `validateDto` | `true` | Enables DTO input validation for generated mappers. |
67
+ | `validateDomain` | `true` | Enables domain output validation for generated mappers. |
68
+
69
+ ## Package Entry Points
70
+
71
+ ```ts
72
+ import { makeValidate } from "runtypex";
73
+ import { makeMapper } from "runtypex/mapper";
74
+ import { generateJSDocFromSpec } from "runtypex/generator";
75
+ import { tsTransformer } from "runtypex/transformer";
76
+ import { vitePlugin } from "runtypex/transformer/vite-plugin";
77
+ ```
78
+
79
+ The package exports ESM and CommonJS builds from `dist/esm` and `dist/cjs`.
80
+
81
+ ## Local Verification
82
+
83
+ Useful verification commands:
84
+
85
+ ```bash
86
+ npm run build
87
+ npx jest --runInBand --watchman=false
88
+ npm run test:esm
89
+ ```
@@ -0,0 +1,88 @@
1
+ # JSDoc Generation
2
+
3
+ JSDoc generation creates interface documentation from mapper metadata. It helps
4
+ editors show where a domain field came from and which DTO or database field it
5
+ represents.
6
+
7
+ ## API
8
+
9
+ ```ts
10
+ import { generateJSDocFromSpec } from "runtypex/generator";
11
+
12
+ const source = generateJSDocFromSpec({
13
+ checker,
14
+ dtoType,
15
+ domainType,
16
+ specNode,
17
+ });
18
+ ```
19
+
20
+ This API is intended for build tooling that already has access to the TypeScript
21
+ program, checker, DTO type, domain type, and mapper spec node.
22
+
23
+ ## Metadata Fields
24
+
25
+ Mapper metadata can include:
26
+
27
+ ```ts
28
+ source("user_id", {
29
+ db: "users.user_id",
30
+ description: "User id",
31
+ dtoDescription: "Identifier returned by the user API.",
32
+ });
33
+ ```
34
+
35
+ Field meanings:
36
+
37
+ | Field | Meaning |
38
+ | --- | --- |
39
+ | `description` | Domain field description. Usually used as the first JSDoc sentence. |
40
+ | `dtoDescription` | Optional explanation attached to the DTO path line. |
41
+ | `db` | Optional database table and column reference. |
42
+
43
+ ## Generated Output
44
+
45
+ Given this domain field:
46
+
47
+ ```ts
48
+ id: source("user_id", {
49
+ db: "users.user_id",
50
+ description: "User id",
51
+ dtoDescription: "Identifier returned by the user API.",
52
+ });
53
+ ```
54
+
55
+ the generated documentation can look like this:
56
+
57
+ ```ts
58
+ /**
59
+ * User id
60
+ *
61
+ * DTO: UserDto.user_id Identifier returned by the user API.
62
+ * DTO type: string
63
+ * DB: users.user_id
64
+ * Domain type: string
65
+ */
66
+ id: string;
67
+ ```
68
+
69
+ ## Policy Integration
70
+
71
+ JSDoc generation can also receive mapper policy options. This lets documentation
72
+ generation fail or warn when the spec violates canonical DTO path naming:
73
+
74
+ ```ts
75
+ generateJSDocFromSpec({
76
+ checker,
77
+ dtoType,
78
+ domainType,
79
+ specNode,
80
+ options: {
81
+ policy,
82
+ policyMode: "error",
83
+ },
84
+ });
85
+ ```
86
+
87
+ Use this when generated docs should reflect the same naming rules as runtime and
88
+ build-time mapper generation.
package/docs/mapper.md ADDED
@@ -0,0 +1,104 @@
1
+ # Mapper
2
+
3
+ The mapper feature converts DTO objects into domain objects with a typed mapping
4
+ spec. It is useful when API or database field names do not match the names used
5
+ inside application code.
6
+
7
+ ## Basic Example
8
+
9
+ ```ts
10
+ import { defineMap, makeMapper, source, transform } from "runtypex/mapper";
11
+
12
+ interface UserDto {
13
+ user_id: string;
14
+ profile: { name: string };
15
+ status: "ACTIVE" | "INACTIVE";
16
+ }
17
+
18
+ interface User {
19
+ id: string;
20
+ displayName: string;
21
+ isActive: boolean;
22
+ }
23
+
24
+ const userMap = defineMap<UserDto, User>()({
25
+ id: source("user_id"),
26
+ displayName: source("profile.name"),
27
+ isActive: transform("status", (value) => value === "ACTIVE"),
28
+ });
29
+
30
+ const toUser = makeMapper<UserDto, User>(userMap);
31
+ ```
32
+
33
+ ## Type-Level Guarantees
34
+
35
+ `defineMap<TDto, TDomain>()` checks the mapping spec at compile time:
36
+
37
+ - every domain field must be present in the mapping spec
38
+ - every `source()` or `transform()` path must exist on the DTO type
39
+ - transform callbacks can return the final domain field value
40
+
41
+ This keeps DTO changes visible at compile time instead of letting a rename fail
42
+ silently at runtime.
43
+
44
+ ## Runtime Behavior
45
+
46
+ Without the transformer, `makeMapper()` interprets the mapping spec at runtime:
47
+
48
+ ```ts
49
+ const user = toUser(dto);
50
+ ```
51
+
52
+ For each domain key, the mapper:
53
+
54
+ 1. reads the DTO value from the configured path
55
+ 2. applies a default when the source value is missing and a default exists
56
+ 3. runs the transform callback when one is provided
57
+ 4. writes the result to the domain output object
58
+
59
+ ## Transformer Behavior
60
+
61
+ With the transformer enabled, this call:
62
+
63
+ ```ts
64
+ const toUser = makeMapper<UserDto, User>(userMap);
65
+ ```
66
+
67
+ is replaced with an inline mapper function. The generated function can validate
68
+ the DTO input before mapping and validate the domain output after mapping.
69
+
70
+ This gives you one mapping declaration while still allowing build-time optimized
71
+ runtime code.
72
+
73
+ ## Metadata
74
+
75
+ Mapping rules can include metadata:
76
+
77
+ ```ts
78
+ id: source("user_id", {
79
+ db: "users.user_id",
80
+ description: "User id",
81
+ dtoDescription: "Identifier returned by the user API.",
82
+ });
83
+ ```
84
+
85
+ Metadata is not required for mapping. It is mainly used by JSDoc generation and
86
+ documentation tooling.
87
+
88
+ ## Typed Helpers
89
+
90
+ Use `mapperHelpers<TDto>()` when helper callbacks need DTO-aware typing:
91
+
92
+ ```ts
93
+ import { mapperHelpers } from "runtypex/mapper";
94
+
95
+ const h = mapperHelpers<UserDto>();
96
+
97
+ const userMap = defineMap<UserDto, User>()({
98
+ id: h.source("user_id"),
99
+ displayName: h.source("profile.name"),
100
+ isActive: h.transform("status", (value, dto) => {
101
+ return dto.status === "ACTIVE";
102
+ }),
103
+ });
104
+ ```
@@ -0,0 +1,78 @@
1
+ # Mapping Policy
2
+
3
+ Mapping policy keeps DTO path to domain field naming consistent across mappers.
4
+ It is useful when the same DTO field appears in multiple domain shapes.
5
+
6
+ ## Problem
7
+
8
+ Without a policy, different mappers can rename the same DTO path differently:
9
+
10
+ ```ts
11
+ const userMap = defineMap<UserDto, User>()({
12
+ userId: source("user_id"),
13
+ });
14
+
15
+ const auditMap = defineMap<UserDto, AuditUser>()({
16
+ realMemberID: source("user_id"),
17
+ });
18
+ ```
19
+
20
+ Both mappings are technically valid, but they make the domain language
21
+ inconsistent.
22
+
23
+ ## Policy Declaration
24
+
25
+ Declare the canonical name once:
26
+
27
+ ```ts
28
+ import { defineMappingPolicy, source } from "runtypex/mapper";
29
+
30
+ const userPolicy = defineMappingPolicy<UserDto>()({
31
+ userId: source("user_id"),
32
+ });
33
+ ```
34
+
35
+ Then pass the policy into a mapper:
36
+
37
+ ```ts
38
+ const toAuditUser = makeMapper<UserDto, AuditUser>(auditMap, {
39
+ policy: userPolicy,
40
+ policyMode: "error",
41
+ });
42
+ ```
43
+
44
+ ## Modes
45
+
46
+ `policyMode` controls how violations are handled:
47
+
48
+ ```ts
49
+ policyMode: "warn"; // default, logs a warning
50
+ policyMode: "error"; // throws an error
51
+ ```
52
+
53
+ Use `"warn"` while introducing a policy into existing code. Use `"error"` when
54
+ the naming convention should be enforced.
55
+
56
+ ## Runtime And Transformer Checks
57
+
58
+ Policy validation runs in both paths:
59
+
60
+ - runtime `makeMapper()` fallback
61
+ - build-time `makeMapper<TDto, TDomain>()` transformer emission
62
+
63
+ That means the policy protects code whether or not the transformer is currently
64
+ configured.
65
+
66
+ ## Duplicate Policy Entries
67
+
68
+ The policy itself must not map the same DTO path to multiple domain names:
69
+
70
+ ```ts
71
+ const invalidPolicy = defineMappingPolicy<UserDto>()({
72
+ userId: source("user_id"),
73
+ realMemberID: source("user_id"),
74
+ });
75
+ ```
76
+
77
+ This is treated as a policy violation because there is no single canonical
78
+ domain name for `user_id`.
@@ -0,0 +1,84 @@
1
+ # Runtime Validation
2
+
3
+ Runtime validation is the core `runtypex` feature. It turns TypeScript types into
4
+ runtime guard functions during build.
5
+
6
+ ## APIs
7
+
8
+ ```ts
9
+ import { makeAssert, makeValidate } from "runtypex";
10
+ ```
11
+
12
+ `makeValidate<T>()` returns a predicate:
13
+
14
+ ```ts
15
+ const isUser = makeValidate<User>();
16
+
17
+ if (isUser(input)) {
18
+ input.id;
19
+ }
20
+ ```
21
+
22
+ `makeAssert<T>()` returns an assertion function:
23
+
24
+ ```ts
25
+ const assertUser = makeAssert<User>();
26
+
27
+ assertUser(input);
28
+ input.id;
29
+ ```
30
+
31
+ ## How It Works
32
+
33
+ You write this:
34
+
35
+ ```ts
36
+ interface User {
37
+ id: number;
38
+ name: string;
39
+ }
40
+
41
+ const isUser = makeValidate<User>();
42
+ ```
43
+
44
+ The transformer reads the `User` type through the TypeScript type checker and
45
+ replaces the call with generated JavaScript:
46
+
47
+ ```js
48
+ const isUser = (v) =>
49
+ typeof v === "object" &&
50
+ v !== null &&
51
+ typeof v.id === "number" &&
52
+ typeof v.name === "string";
53
+ ```
54
+
55
+ No runtime reflection is required. The generated code is plain JavaScript.
56
+
57
+ ## Production Removal
58
+
59
+ When `removeInProd: true` is enabled and `NODE_ENV` is `production`,
60
+ validators are replaced with no-op equivalents:
61
+
62
+ ```ts
63
+ makeValidate<T>(); // (_) => true
64
+ makeAssert<T>(); // (_) => {}
65
+ ```
66
+
67
+ Use this only when runtime validation is a development-time safety net and not a
68
+ production boundary.
69
+
70
+ ## Supported Shape Examples
71
+
72
+ The current emitter covers common TypeScript shapes:
73
+
74
+ - primitives
75
+ - object and interface properties
76
+ - optional properties
77
+ - arrays
78
+ - tuples
79
+ - unions
80
+ - intersections
81
+ - literal types
82
+ - enums
83
+
84
+ See the emitter tests for exact behavior around edge cases.