@mirascript/typed 0.1.76 → 0.1.79

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 (99) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/json/impl.d.ts +8 -0
  6. package/dist/json/impl.d.ts.map +1 -0
  7. package/dist/json/impl.js +51 -0
  8. package/dist/json/impl.js.map +1 -0
  9. package/dist/{json.d.ts → json/index.d.ts} +2 -2
  10. package/dist/json/index.d.ts.map +1 -0
  11. package/dist/json/index.js +17 -0
  12. package/dist/json/index.js.map +1 -0
  13. package/dist/json/literal.d.ts +9 -0
  14. package/dist/json/literal.d.ts.map +1 -0
  15. package/dist/json/literal.js +16 -0
  16. package/dist/json/literal.js.map +1 -0
  17. package/dist/json/record.d.ts +6 -0
  18. package/dist/json/record.d.ts.map +1 -0
  19. package/dist/json/record.js +66 -0
  20. package/dist/json/record.js.map +1 -0
  21. package/dist/json/string.d.ts +5 -0
  22. package/dist/json/string.d.ts.map +1 -0
  23. package/dist/json/string.js +28 -0
  24. package/dist/json/string.js.map +1 -0
  25. package/dist/json/template.d.ts +8 -0
  26. package/dist/json/template.d.ts.map +1 -0
  27. package/dist/json/template.js +72 -0
  28. package/dist/json/template.js.map +1 -0
  29. package/dist/json/tuple.d.ts +8 -0
  30. package/dist/json/tuple.d.ts.map +1 -0
  31. package/dist/json/tuple.js +52 -0
  32. package/dist/json/tuple.js.map +1 -0
  33. package/dist/json/union-intersection.d.ts +8 -0
  34. package/dist/json/union-intersection.d.ts.map +1 -0
  35. package/dist/json/union-intersection.js +40 -0
  36. package/dist/json/union-intersection.js.map +1 -0
  37. package/dist/simplifier/dedup.d.ts +4 -0
  38. package/dist/simplifier/dedup.d.ts.map +1 -0
  39. package/dist/simplifier/dedup.js +61 -0
  40. package/dist/simplifier/dedup.js.map +1 -0
  41. package/dist/simplifier/impl.d.ts +7 -0
  42. package/dist/simplifier/impl.d.ts.map +1 -0
  43. package/dist/simplifier/impl.js +51 -0
  44. package/dist/simplifier/impl.js.map +1 -0
  45. package/dist/{simplifier.d.ts → simplifier/index.d.ts} +3 -5
  46. package/dist/simplifier/index.d.ts.map +1 -0
  47. package/dist/simplifier/index.js +22 -0
  48. package/dist/simplifier/index.js.map +1 -0
  49. package/dist/simplifier/intersection.d.ts +5 -0
  50. package/dist/simplifier/intersection.d.ts.map +1 -0
  51. package/dist/simplifier/intersection.js +126 -0
  52. package/dist/simplifier/intersection.js.map +1 -0
  53. package/dist/simplifier/record.d.ts +5 -0
  54. package/dist/simplifier/record.d.ts.map +1 -0
  55. package/dist/simplifier/record.js +24 -0
  56. package/dist/simplifier/record.js.map +1 -0
  57. package/dist/simplifier/top-type.d.ts +5 -0
  58. package/dist/simplifier/top-type.d.ts.map +1 -0
  59. package/dist/simplifier/top-type.js +9 -0
  60. package/dist/simplifier/top-type.js.map +1 -0
  61. package/dist/simplifier/tuple.d.ts +5 -0
  62. package/dist/simplifier/tuple.d.ts.map +1 -0
  63. package/dist/simplifier/tuple.js +17 -0
  64. package/dist/simplifier/tuple.js.map +1 -0
  65. package/dist/simplifier/union.d.ts +5 -0
  66. package/dist/simplifier/union.d.ts.map +1 -0
  67. package/dist/simplifier/union.js +47 -0
  68. package/dist/simplifier/union.js.map +1 -0
  69. package/dist/simplifier/utils.d.ts +12 -0
  70. package/dist/simplifier/utils.d.ts.map +1 -0
  71. package/dist/simplifier/utils.js +17 -0
  72. package/dist/simplifier/utils.js.map +1 -0
  73. package/package.json +5 -5
  74. package/src/index.ts +2 -2
  75. package/src/json/impl.ts +57 -0
  76. package/src/json/index.ts +28 -0
  77. package/src/json/literal.ts +20 -0
  78. package/src/json/record.ts +70 -0
  79. package/src/json/string.ts +30 -0
  80. package/src/json/template.ts +71 -0
  81. package/src/json/tuple.ts +52 -0
  82. package/src/json/union-intersection.ts +43 -0
  83. package/src/simplifier/dedup.ts +61 -0
  84. package/src/simplifier/impl.ts +64 -0
  85. package/src/simplifier/index.ts +67 -0
  86. package/src/simplifier/intersection.ts +129 -0
  87. package/src/simplifier/record.ts +24 -0
  88. package/src/simplifier/top-type.ts +9 -0
  89. package/src/simplifier/tuple.ts +20 -0
  90. package/src/simplifier/union.ts +45 -0
  91. package/src/simplifier/utils.ts +21 -0
  92. package/dist/json.d.ts.map +0 -1
  93. package/dist/json.js +0 -314
  94. package/dist/json.js.map +0 -1
  95. package/dist/simplifier.d.ts.map +0 -1
  96. package/dist/simplifier.js +0 -332
  97. package/dist/simplifier.js.map +0 -1
  98. package/src/json.ts +0 -323
  99. package/src/simplifier.ts +0 -404
@@ -0,0 +1,70 @@
1
+ import type { JSONSchema } from 'json-schema-typed';
2
+ import type { RecordField, RecordType, Type } from '../parser.js';
3
+ import { toJSONSchemaImpl, type ToJSONSchemaOptionsImpl } from './impl.js';
4
+ import { templatePartPattern, RE_ANY } from './template.js';
5
+ import { isLiteralType } from './literal.js';
6
+
7
+ /** Convert fields record into JSON Schema */
8
+ function fieldsRecord(fields: readonly RecordField[], options: ToJSONSchemaOptionsImpl): JSONSchema {
9
+ const properties: Record<string, JSONSchema> = {};
10
+ const required: string[] = [];
11
+ for (const field of fields) {
12
+ properties[field.name] = toJSONSchemaImpl(field.type, options);
13
+ if (!options.loose && !field.optional) {
14
+ required.push(field.name);
15
+ }
16
+ }
17
+ const schema: JSONSchema = {
18
+ type: 'object',
19
+ properties,
20
+ additionalProperties: options.loose,
21
+ };
22
+ if (required.length > 0) {
23
+ schema.required = required;
24
+ }
25
+ return schema;
26
+ }
27
+
28
+ /** Convert generic record into JSON Schema */
29
+ function genericRecord(key: Type, value: Type, options: ToJSONSchemaOptionsImpl): JSONSchema {
30
+ const valueSchema = toJSONSchemaImpl(value, options);
31
+
32
+ if (typeof key == 'object') {
33
+ if (isLiteralType(key)) {
34
+ const schema: JSONSchema = {
35
+ type: 'object',
36
+ properties: { [String(key.value)]: valueSchema },
37
+ additionalProperties: options.loose,
38
+ };
39
+ return schema;
40
+ } else if (key.kind === 'union' && key.types.every(isLiteralType)) {
41
+ const schema: JSONSchema = {
42
+ type: 'object',
43
+ properties: Object.fromEntries(key.types.map((t) => [String(t.value), valueSchema])),
44
+ additionalProperties: options.loose,
45
+ };
46
+ return schema;
47
+ }
48
+ }
49
+ const pattern = templatePartPattern(key, false);
50
+ if (pattern === RE_ANY) {
51
+ return {
52
+ type: 'object',
53
+ additionalProperties: valueSchema,
54
+ };
55
+ }
56
+ const schema: JSONSchema = {
57
+ type: 'object',
58
+ patternProperties: { [`^${pattern}$`]: valueSchema },
59
+ additionalProperties: options.loose,
60
+ };
61
+ return schema;
62
+ }
63
+
64
+ /** Converts a RecordType into JSON Schema */
65
+ export function record(simplified: RecordType, options: ToJSONSchemaOptionsImpl): JSONSchema {
66
+ if ('fields' in simplified) {
67
+ return fieldsRecord(simplified.fields, options);
68
+ }
69
+ return genericRecord(simplified.key ?? 'string', simplified.value, options);
70
+ }
@@ -0,0 +1,30 @@
1
+ import type { JSONSchema } from 'json-schema-typed';
2
+ import type { KnownType, NamedType } from '../parser.js';
3
+
4
+ /** Converts a KnownType or NamedType into JSON Schema */
5
+ export function string(type: KnownType | NamedType): JSONSchema {
6
+ switch (type) {
7
+ case 'string':
8
+ return { type: 'string' };
9
+ case 'number':
10
+ return { type: 'number' };
11
+ case 'boolean':
12
+ return { type: 'boolean' };
13
+ case 'nil':
14
+ return { type: 'null' };
15
+ case 'array':
16
+ return { type: 'array', items: true };
17
+ case 'record':
18
+ return { type: 'object' };
19
+ case 'extern':
20
+ return true;
21
+ case 'any':
22
+ return true;
23
+ case 'unknown':
24
+ return true;
25
+ case 'never':
26
+ return false;
27
+ default:
28
+ return true;
29
+ }
30
+ }
@@ -0,0 +1,71 @@
1
+ import type { JSONSchema } from 'json-schema-typed';
2
+ import { REG_NUMBER } from '@mirascript/constants';
3
+ import type { TemplateType, Type } from '../parser.js';
4
+
5
+ /** Escapes a literal string for use in a regular expression */
6
+ function escapeRegex(value: string): string {
7
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
8
+ }
9
+
10
+ export const RE_ANY = '.*?';
11
+ const RE_NUMBER = REG_NUMBER.source;
12
+ const RE_BOOLEAN = 'true|false';
13
+
14
+ /** Converts a template interpolation part into a regex pattern fragment */
15
+ function templatePartPatternImpl(part: Type): string {
16
+ if (typeof part == 'symbol') {
17
+ return RE_ANY;
18
+ }
19
+ if (typeof part == 'string') {
20
+ if (part === 'number') return RE_NUMBER;
21
+ if (part === 'boolean') return RE_BOOLEAN;
22
+ if (part === 'nil' || part === 'never') return '';
23
+ return RE_ANY;
24
+ }
25
+ if (part.kind === 'literal') {
26
+ if (typeof part.value === 'boolean') {
27
+ return String(part.value);
28
+ } else {
29
+ return escapeRegex(part.value);
30
+ }
31
+ }
32
+ if (part.kind === 'union') {
33
+ const patterns = new Set(part.types.map((p) => templatePartPatternImpl(p)));
34
+ if (patterns.has(RE_ANY)) {
35
+ return RE_ANY;
36
+ } else {
37
+ const hasEmpty = patterns.delete('');
38
+ let result = Array.from(patterns).join('|');
39
+ if (hasEmpty) {
40
+ result = `(${result})?`;
41
+ }
42
+ return result;
43
+ }
44
+ }
45
+ if (part.kind === 'intersection') {
46
+ // Regex intersection is not representable in general; keep behavior conservative.
47
+ return RE_ANY;
48
+ }
49
+ return RE_ANY;
50
+ }
51
+
52
+ /** Converts a template interpolation part into a regex pattern fragment */
53
+ export function templatePartPattern(part: Type, grouping: boolean): string {
54
+ const result = templatePartPatternImpl(part);
55
+ if (!grouping) return result;
56
+ if (result.startsWith('(')) return result;
57
+ return `(${result})`;
58
+ }
59
+
60
+ /** Converts a TemplateType into a JSON Schema pattern */
61
+ export function template(type: TemplateType): JSONSchema {
62
+ const pattern = `^${type.parts
63
+ .map((p) => {
64
+ if (typeof p == 'object' && p.kind === 'literal' && typeof p.value === 'string') {
65
+ return templatePartPattern(p, false);
66
+ }
67
+ return templatePartPattern(p, true);
68
+ })
69
+ .join('')}$`;
70
+ return { type: 'string', pattern };
71
+ }
@@ -0,0 +1,52 @@
1
+ import type { JSONSchema } from 'json-schema-typed';
2
+ import type { TupleType } from '../parser.js';
3
+ import { toJSONSchemaImpl, type ToJSONSchemaOptionsImpl } from './impl.js';
4
+
5
+ /**
6
+ * Converts a TupleType into JSON Schema
7
+ */
8
+ export function tuple(tuple: TupleType, options: ToJSONSchemaOptionsImpl): JSONSchema {
9
+ // Collect element schemas, unwrapping array from rest elements (..T[] → T).
10
+ // Non-array rest (e.g. ..string) is treated as any ({}).
11
+ const elementSchemas = tuple.elements.map((e) => {
12
+ const t = e.type;
13
+ if (!e.spread) return toJSONSchemaImpl(t, options);
14
+ if (typeof t == 'object' && t.kind === 'array') {
15
+ return toJSONSchemaImpl(t.element, options);
16
+ }
17
+ return true;
18
+ });
19
+
20
+ const prefixItems = [];
21
+ let i = 0;
22
+ for (; i < elementSchemas.length; i++) {
23
+ const el = tuple.elements[i]!;
24
+ if (el.spread) break;
25
+ prefixItems.push(elementSchemas[i]!);
26
+ }
27
+
28
+ const items = [];
29
+ let hasAny = false;
30
+ for (; i < elementSchemas.length; i++) {
31
+ const schema = elementSchemas[i]!;
32
+ items.push(schema);
33
+ if (schema === true) {
34
+ hasAny = true;
35
+ }
36
+ }
37
+
38
+ const schema: JSONSchema = { type: 'array' };
39
+ if (prefixItems.length > 0) {
40
+ schema.prefixItems = prefixItems;
41
+ }
42
+ if (hasAny) {
43
+ schema.items = true;
44
+ } else if (items.length > 1) {
45
+ schema.items = { anyOf: items };
46
+ } else if (items.length === 1) {
47
+ schema.items = items[0]!;
48
+ } else {
49
+ schema.items = options.loose;
50
+ }
51
+ return schema;
52
+ }
@@ -0,0 +1,43 @@
1
+ import type { JSONSchema } from 'json-schema-typed';
2
+ import type { IntersectionType, LiteralType, UnionType } from '../parser.js';
3
+ import { type ToJSONSchemaOptionsImpl, toJSONSchemaImpl } from './impl.js';
4
+ import { isLiteralType, literalEnum } from './literal.js';
5
+
6
+ /** Converts a union type to JSON representation */
7
+ export function union(union: UnionType, options: ToJSONSchemaOptionsImpl): JSONSchema {
8
+ const anyOf: JSONSchema[] = [];
9
+ const literals: LiteralType[] = [];
10
+ for (const t of union.types) {
11
+ if (isLiteralType(t)) {
12
+ literals.push(t);
13
+ continue;
14
+ }
15
+ const child = toJSONSchemaImpl(t, options);
16
+ if (child === true) {
17
+ return true;
18
+ }
19
+ if (child === false) {
20
+ continue;
21
+ }
22
+ anyOf.push(child);
23
+ }
24
+ if (literals.length > 0) {
25
+ anyOf.push(literalEnum(literals));
26
+ }
27
+ if (anyOf.length === 0) {
28
+ return false;
29
+ }
30
+ if (anyOf.length === 1) {
31
+ return anyOf[0]!;
32
+ }
33
+ return { anyOf };
34
+ }
35
+
36
+ /** Converts an intersection type to JSON representation */
37
+ export function intersection(intersection: IntersectionType, options: ToJSONSchemaOptionsImpl): JSONSchema {
38
+ const allOf = intersection.types.map((t) => toJSONSchemaImpl(t, options));
39
+ if (allOf.length === 1) {
40
+ return allOf[0]!;
41
+ }
42
+ return { allOf };
43
+ }
@@ -0,0 +1,61 @@
1
+ import type { Type } from '../parser.js';
2
+
3
+ /** Builds a stable key for type-level deduplication within one simplify call. */
4
+ function getTypeDedupKey(type: Type, symbols: Map<symbol, number>): string {
5
+ if (typeof type === 'string') return `string:${type}`;
6
+ if (typeof type === 'symbol') {
7
+ const existing = symbols.get(type);
8
+ if (existing != null) return `symbol:${existing}`;
9
+ const next = symbols.size + 1;
10
+ symbols.set(type, next);
11
+ return `symbol:${next}`;
12
+ }
13
+ switch (type.kind) {
14
+ case 'array':
15
+ return `array:${getTypeDedupKey(type.element, symbols)}`;
16
+ case 'union':
17
+ return `union:[${type.types.map((t) => getTypeDedupKey(t, symbols)).join(',')}]`;
18
+ case 'intersection':
19
+ return `intersection:[${type.types.map((t) => getTypeDedupKey(t, symbols)).join(',')}]`;
20
+ case 'record':
21
+ if ('fields' in type) {
22
+ return `recordFields:[${type.fields
23
+ .map((f) => `${f.name}:${String(Boolean(f.optional))}:${getTypeDedupKey(f.type, symbols)}`)
24
+ .join(',')}]`;
25
+ }
26
+ return `recordKV:${type.key == null ? 'none' : getTypeDedupKey(type.key, symbols)}:${getTypeDedupKey(type.value, symbols)}`;
27
+ case 'literal':
28
+ return `literal:${typeof type.value}:${String(type.value)}`;
29
+ case 'template':
30
+ return `template:[${type.parts.map((p) => getTypeDedupKey(p, symbols)).join(',')}]`;
31
+ case 'function':
32
+ return `function:${
33
+ type.name ?? ''
34
+ }:<${(type.typeParams ?? []).map((p) => getTypeDedupKey(p, symbols)).join(',')}>(${type.params
35
+ .map((p) => `${p.name}:${String(Boolean(p.spread))}:${getTypeDedupKey(p.type, symbols)}`)
36
+ .join(',')})=>${type.returns == null ? 'void' : getTypeDedupKey(type.returns, symbols)}`;
37
+ case 'tuple':
38
+ return `tuple:[${type.elements
39
+ .map((e) => `${String(Boolean(e.spread))}:${getTypeDedupKey(e.type, symbols)}`)
40
+ .join(',')}]`;
41
+ case 'reflection':
42
+ return `reflection:${type.name}`;
43
+ default:
44
+ return 'unknown';
45
+ }
46
+ }
47
+
48
+ /** Removes duplicate members from union/intersection type member lists. */
49
+ export function deduplicateTypeMembers(types: Type[]): Type[] {
50
+ if (types.length <= 1) return types;
51
+ const symbols = new Map<symbol, number>();
52
+ const seen = new Set<string>();
53
+ const result: Type[] = [];
54
+ for (const type of types) {
55
+ const key = getTypeDedupKey(type, symbols);
56
+ if (seen.has(key)) continue;
57
+ seen.add(key);
58
+ result.push(type);
59
+ }
60
+ return result;
61
+ }
@@ -0,0 +1,64 @@
1
+ import type { Type } from '../parser.js';
2
+ import { simplifyIntersection } from './intersection.js';
3
+ import type { SimplifyOptions } from './index.js';
4
+ import { simplifyUnion } from './union.js';
5
+ import { simplifyRecord } from './record.js';
6
+ import { simplifyTuple } from './tuple.js';
7
+
8
+ /** Options for simplifyImpl */
9
+ export type SimplifyImplOptions = Required<SimplifyOptions>;
10
+
11
+ /** Simplifies a Type AST in place, optionally disabling individual normalization passes. */
12
+ export function simplifyImpl(type: Type, options: SimplifyImplOptions): Type {
13
+ if (typeof type == 'symbol' || typeof type == 'string') {
14
+ return type;
15
+ }
16
+
17
+ if (type.kind === 'array') {
18
+ type.element = simplifyImpl(type.element, options);
19
+ if (options.normalizeGenericArray && (type.element === 'any' || type.element === 'unknown')) {
20
+ return 'array';
21
+ }
22
+ return type;
23
+ }
24
+
25
+ if (type.kind === 'function') {
26
+ for (const param of type.params) {
27
+ param.type = simplifyImpl(param.type, options);
28
+ }
29
+ if (type.returns != null) type.returns = simplifyImpl(type.returns, options);
30
+ return type;
31
+ }
32
+
33
+ if (type.kind === 'literal') {
34
+ return type;
35
+ }
36
+
37
+ if (type.kind === 'template') {
38
+ type.parts = type.parts.map((part) => simplifyImpl(part, options));
39
+ return type;
40
+ }
41
+
42
+ if (type.kind === 'tuple') {
43
+ return simplifyTuple(type, options);
44
+ }
45
+
46
+ if (type.kind === 'reflection') {
47
+ return type;
48
+ }
49
+
50
+ if (type.kind === 'record') {
51
+ return simplifyRecord(type, options);
52
+ }
53
+
54
+ if (type.kind === 'union') {
55
+ return simplifyUnion(type, options);
56
+ }
57
+
58
+ if (type.kind === 'intersection') {
59
+ return simplifyIntersection(type, options);
60
+ }
61
+ /* c8 ignore next 3 */
62
+ (type) satisfies never;
63
+ return type;
64
+ }
@@ -0,0 +1,67 @@
1
+ import type { Type } from '../parser.js';
2
+ import { simplifyImpl } from './impl.js';
3
+ import type { TopType } from './top-type.js';
4
+
5
+ /** Controls which type simplifications are applied. */
6
+ export interface SimplifyOptions {
7
+ /** Flatten nested union nodes. */
8
+ flattenUnions?: boolean;
9
+ /** Flatten nested intersection nodes. */
10
+ flattenIntersections?: boolean;
11
+ /** Remove duplicate members inside union nodes. */
12
+ deduplicateUnions?: boolean;
13
+ /** Remove duplicate members inside intersection nodes. */
14
+ deduplicateIntersections?: boolean;
15
+ /** Remove single-member union nodes. */
16
+ unwrapSingleUnion?: boolean;
17
+ /** Remove single-member intersection nodes. */
18
+ unwrapSingleIntersection?: boolean;
19
+ /** Distribute intersections over unions. */
20
+ distributeIntersectionsOverUnions?: boolean;
21
+ /** Merge intersections of explicit record fields. */
22
+ mergeRecordIntersections?: boolean;
23
+ /** Inline tuple spread elements (..[A, B] → A, B). */
24
+ expandTupleSpreads?: boolean;
25
+ /**
26
+ * Eliminate / absorb top types in unions.
27
+ * - `unknown | T` → `unknown`
28
+ * - `any | T` → `any`
29
+ * - `T | never` → `T`
30
+ * `true` enables all, or pass a subset of `'unknown' | 'never' | 'any'`.
31
+ */
32
+ simplifyTopTypesInUnions?: boolean | TopType[];
33
+ /**
34
+ * Eliminate / absorb top types in intersections.
35
+ * - `never & T` → `never`
36
+ * - `T & unknown` → `T`
37
+ * - `T & any` → `T`
38
+ * `true` enables all, or pass a subset of `'unknown' | 'never' | 'any'`.
39
+ */
40
+ simplifyTopTypesInIntersections?: boolean | TopType[];
41
+ /** `record<string, V>` → `record<V>` (string is the default key). */
42
+ normalizeGenericRecord?: boolean;
43
+ /** `array<any | unknown>` → `array` (no element constraint). */
44
+ normalizeGenericArray?: boolean;
45
+ }
46
+
47
+ const DEFAULT_OPTIONS: Required<SimplifyOptions> = {
48
+ flattenUnions: true,
49
+ flattenIntersections: true,
50
+ deduplicateUnions: true,
51
+ deduplicateIntersections: true,
52
+ unwrapSingleUnion: true,
53
+ unwrapSingleIntersection: true,
54
+ distributeIntersectionsOverUnions: true,
55
+ mergeRecordIntersections: true,
56
+ expandTupleSpreads: true,
57
+ simplifyTopTypesInUnions: true,
58
+ simplifyTopTypesInIntersections: true,
59
+ normalizeGenericRecord: true,
60
+ normalizeGenericArray: true,
61
+ };
62
+
63
+ /** Simplifies a Type AST in place, optionally disabling individual normalization passes. */
64
+ export function simplify(type: Type, options?: SimplifyOptions): Type {
65
+ const config = { ...DEFAULT_OPTIONS, ...options };
66
+ return simplifyImpl(type, config);
67
+ }
@@ -0,0 +1,129 @@
1
+ import type { IntersectionType, Type, RecordType, RecordField } from '../parser.js';
2
+ import { deduplicateTypeMembers } from './dedup.js';
3
+ import { type SimplifyImplOptions, simplifyImpl } from './impl.js';
4
+ import { resolveTopTypes } from './top-type.js';
5
+ import { isFieldRecordType, isUnionType, isIntersectionType } from './utils.js';
6
+
7
+ /** Merges explicit record fields across an intersection. */
8
+ function mergeRecordFieldIntersections(types: Array<Extract<RecordType, { fields: RecordField[] }>>): Type {
9
+ const merged = new Map<string, { optional: boolean; type: Type }>();
10
+ for (const record of types) {
11
+ for (const field of record.fields) {
12
+ const prev = merged.get(field.name);
13
+ if (prev == null) {
14
+ merged.set(field.name, {
15
+ optional: field.optional ?? false,
16
+ type: field.type,
17
+ });
18
+ continue;
19
+ }
20
+ merged.set(field.name, {
21
+ optional: (prev.optional ?? false) && (field.optional ?? false),
22
+ type: {
23
+ kind: 'intersection',
24
+ types: [prev.type, field.type],
25
+ },
26
+ });
27
+ }
28
+ }
29
+
30
+ return {
31
+ kind: 'record',
32
+ fields: Array.from(merged.entries()).map(([name, field]) => ({
33
+ name,
34
+ optional: field.optional,
35
+ type: field.type,
36
+ })),
37
+ };
38
+ }
39
+
40
+ /** Flattens nested intersection nodes when the corresponding option is enabled. */
41
+ function flattenIntersectionTypes(type: IntersectionType, options: SimplifyImplOptions): Type[] {
42
+ const types = type.types.map((item) => simplifyImpl(item, options));
43
+ if (!options.flattenIntersections) return types;
44
+ const result: Type[] = [];
45
+ for (const type of types) {
46
+ if (isIntersectionType(type)) {
47
+ result.push(...flattenIntersectionTypes(type, options));
48
+ } else {
49
+ result.push(type);
50
+ }
51
+ }
52
+ return result;
53
+ }
54
+
55
+ /** Distributes intersections over unions using a cartesian product. */
56
+ function distributeIntersectionsOverUnions(types: Type[], options: SimplifyImplOptions): Type {
57
+ let combinations: Type[][] = [[]];
58
+ for (const type of types) {
59
+ const choices = isUnionType(type) ? type.types : [type];
60
+ const next: Type[][] = [];
61
+ for (const combo of combinations) {
62
+ for (const choice of choices) {
63
+ next.push([...combo, choice]);
64
+ }
65
+ }
66
+ combinations = next;
67
+ }
68
+
69
+ const opt = { ...options, distributeIntersectionsOverUnions: false };
70
+ const branches = combinations.map((combination) => simplifyImpl({ kind: 'intersection', types: combination }, opt));
71
+ if (branches.length === 1) return branches[0]!;
72
+ return { kind: 'union', types: branches };
73
+ }
74
+
75
+ /** Simplifies an intersection type */
76
+ export function simplifyIntersection(type: IntersectionType, options: SimplifyImplOptions): Type {
77
+ let simplifiedTypes = flattenIntersectionTypes(type, options);
78
+
79
+ // Top-type elimination: never & T → never, T & unknown → T, T & any → T
80
+ const topTypes = resolveTopTypes(options.simplifyTopTypesInIntersections);
81
+ if (topTypes.length > 0) {
82
+ const typeNames = new Set(simplifiedTypes.filter((t): t is string => typeof t === 'string'));
83
+ if (topTypes.includes('never') && typeNames.has('never')) return 'never';
84
+ let filtered = false;
85
+ if (topTypes.includes('unknown') && typeNames.has('unknown')) {
86
+ simplifiedTypes = simplifiedTypes.filter((t) => t !== 'unknown');
87
+ filtered = true;
88
+ }
89
+ if (topTypes.includes('any') && typeNames.has('any')) {
90
+ simplifiedTypes = simplifiedTypes.filter((t) => t !== 'any');
91
+ filtered = true;
92
+ }
93
+ if (filtered) {
94
+ if (simplifiedTypes.length === 1) return simplifiedTypes[0]!;
95
+ if (simplifiedTypes.length === 0) {
96
+ // All members were the same eliminated top type
97
+ return typeNames.has('any') ? 'any' : 'unknown';
98
+ }
99
+ }
100
+ }
101
+
102
+ if (options.deduplicateIntersections) {
103
+ simplifiedTypes = deduplicateTypeMembers(simplifiedTypes);
104
+ }
105
+ if (options.distributeIntersectionsOverUnions && simplifiedTypes.some(isUnionType)) {
106
+ return distributeIntersectionsOverUnions(simplifiedTypes, options);
107
+ }
108
+ if (options.mergeRecordIntersections) {
109
+ const recordTypes = simplifiedTypes.filter(isFieldRecordType);
110
+ if (recordTypes.length >= 2) {
111
+ const nonRecordTypes = simplifiedTypes.filter((item) => !isFieldRecordType(item));
112
+ const mergedRecord = simplifyImpl(mergeRecordFieldIntersections(recordTypes), options);
113
+ let mergedTypes = [mergedRecord, ...nonRecordTypes];
114
+ if (options.deduplicateIntersections) {
115
+ mergedTypes = deduplicateTypeMembers(mergedTypes);
116
+ }
117
+ if (options.unwrapSingleIntersection && mergedTypes.length === 1) {
118
+ return mergedTypes[0]!;
119
+ }
120
+ type.types = mergedTypes;
121
+ return type;
122
+ }
123
+ }
124
+ if (options.unwrapSingleIntersection && simplifiedTypes.length === 1) {
125
+ return simplifiedTypes[0]!;
126
+ }
127
+ type.types = simplifiedTypes;
128
+ return type;
129
+ }
@@ -0,0 +1,24 @@
1
+ import type { RecordField, RecordType } from '../parser.js';
2
+ import { type SimplifyImplOptions, simplifyImpl } from './impl.js';
3
+
4
+ /** Simplifies a record field recursively. */
5
+ function simplifyRecordField(field: RecordField, options: SimplifyImplOptions): RecordField {
6
+ return {
7
+ ...field,
8
+ type: simplifyImpl(field.type, options),
9
+ };
10
+ }
11
+
12
+ /** Simplifies a record recursively. */
13
+ export function simplifyRecord(record: RecordType, options: SimplifyImplOptions): RecordType {
14
+ if ('fields' in record) {
15
+ record.fields = record.fields.map((field) => simplifyRecordField(field, options));
16
+ } else {
17
+ if (record.key != null) record.key = simplifyImpl(record.key, options);
18
+ record.value = simplifyImpl(record.value, options);
19
+ if (options.normalizeGenericRecord && record.key === 'string') {
20
+ delete record.key;
21
+ }
22
+ }
23
+ return record;
24
+ }
@@ -0,0 +1,9 @@
1
+ /** Top types that can be absorbed / eliminated in simplification. */
2
+ export type TopType = 'unknown' | 'never' | 'any';
3
+
4
+ /** Resolves the top types option into an array of top types. */
5
+ export function resolveTopTypes(value: boolean | TopType[] | undefined): TopType[] {
6
+ if (value === false || value == null) return [];
7
+ if (value === true) return ['unknown', 'never', 'any'];
8
+ return value;
9
+ }
@@ -0,0 +1,20 @@
1
+ import type { TupleType } from '../parser.js';
2
+ import { simplifyImpl, type SimplifyImplOptions } from './impl.js';
3
+
4
+ /** Simplifies tuple types */
5
+ export function simplifyTuple(type: TupleType, options: SimplifyImplOptions): TupleType {
6
+ type.elements = type.elements.flatMap((element) => {
7
+ const simplifiedType = simplifyImpl(element.type, options);
8
+ if (
9
+ options.expandTupleSpreads &&
10
+ element.spread &&
11
+ typeof simplifiedType == 'object' &&
12
+ simplifiedType.kind === 'tuple'
13
+ ) {
14
+ // Inline tuple spread: ..[A, B] → A, B
15
+ return simplifiedType.elements;
16
+ }
17
+ return [{ ...element, type: simplifiedType }];
18
+ });
19
+ return type;
20
+ }