tjs-lang 0.6.13 → 0.6.15

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,180 @@
1
+ # FunctionPredicate: Design Notes
2
+
3
+ _First-class function types in TJS, using the same pattern as Type/Generic._
4
+
5
+ ---
6
+
7
+ ## The Problem
8
+
9
+ TJS has no way to express "this parameter must be a function with this
10
+ signature." Currently:
11
+
12
+ - `() => void` in TypeScript becomes `undefined` in fromTS output
13
+ - There's no TJS syntax for function-typed parameters
14
+ - Callbacks, event handlers, and higher-order functions lose their
15
+ type information at the boundary
16
+
17
+ ## Design Principles
18
+
19
+ 1. **Functions are values** — a function should be usable as a type example,
20
+ just like `0` means "integer" and `''` means "string"
21
+ 2. **FunctionPredicate should work like Type/Generic** — same pattern of
22
+ predicate-based checking, introspection via metadata
23
+ 3. **The return contract is part of the type** — `->`, `-?`, and `-!` are
24
+ meaningful distinctions in the function's contract
25
+
26
+ ## The Three Return Contracts
27
+
28
+ | Marker | Name | Meaning |
29
+ |--------|------|---------|
30
+ | `->` | `returns` | Verified at transpile time (signature test) |
31
+ | `-?` | `checkedReturns` | Verified at transpile time AND runtime |
32
+ | `-!` | `assertReturns` | Declared but not verified (metadata only) |
33
+
34
+ These are not just build options — they describe the **trust level** of
35
+ the function's return type. A function with `-?` makes a stronger promise
36
+ than one with `-!`.
37
+
38
+ ## Syntax: Function as Type Example
39
+
40
+ The most TJS-idiomatic approach — a function IS its own type:
41
+
42
+ ```tjs
43
+ // This function's signature IS a type
44
+ function formatter(input: '', options: { locale: 'en' }) -? '' {
45
+ return input
46
+ }
47
+
48
+ // fn must match formatter's contract
49
+ function process(fn: formatter) {
50
+ const result = fn('hello', { locale: 'fr' })
51
+ }
52
+ ```
53
+
54
+ The runtime check for `fn: formatter`:
55
+ 1. `typeof fn === 'function'`
56
+ 2. `fn.__tjs` exists (it's a TJS-typed function)
57
+ 3. `fn.__tjs.params` shape-matches `formatter.__tjs.params`
58
+ 4. `fn.__tjs.returns` matches `formatter.__tjs.returns`
59
+
60
+ Untyped functions (no `__tjs`) would fail the check — they don't have
61
+ the metadata to verify against. Use `!` (unsafe) to skip the check for
62
+ interop with plain JS callbacks.
63
+
64
+ ## Syntax: Explicit FunctionPredicate
65
+
66
+ For cases where you want to declare a function type without writing an
67
+ example function:
68
+
69
+ ```tjs
70
+ FunctionPredicate Formatter {
71
+ description: 'formats a string with locale options'
72
+ params: { input: '', options: { locale: 'en' } }
73
+ returns: ''
74
+ }
75
+
76
+ // Or with checked returns:
77
+ FunctionPredicate Validator {
78
+ params: { value: null }
79
+ checkedReturns: false
80
+ }
81
+
82
+ // Or declared-only returns:
83
+ FunctionPredicate Callback {
84
+ params: { event: { type: '', target: null } }
85
+ assertReturns: undefined
86
+ }
87
+ ```
88
+
89
+ ## Syntax: FunctionPredicate from Function
90
+
91
+ Create a type from an existing function's metadata:
92
+
93
+ ```tjs
94
+ function myFormatter(input: '', options: { locale: 'en' }) -? '' {
95
+ return input
96
+ }
97
+
98
+ // Extract the type from the function
99
+ FunctionPredicate Formatter(myFormatter, 'string formatter with locale')
100
+ ```
101
+
102
+ This is analogous to `Type Name 'example'` — the function itself is the
103
+ example value, and its `__tjs` metadata defines the type.
104
+
105
+ ## Runtime Representation
106
+
107
+ A FunctionPredicate at runtime would be an object with:
108
+
109
+ ```javascript
110
+ {
111
+ check(fn) { ... }, // returns boolean
112
+ params: { ... }, // param descriptors
113
+ returns: { ... }, // return type descriptor
114
+ returnContract: 'checked' | 'returns' | 'assert',
115
+ description: '...',
116
+ default: exampleFn, // the example function, if provided
117
+ }
118
+ ```
119
+
120
+ This matches the shape of `Type()` — `check`, `default`, `description`.
121
+
122
+ ## Validation Levels
123
+
124
+ When checking `fn: SomeType` where SomeType is a FunctionPredicate:
125
+
126
+ | Check | What it verifies |
127
+ |-------|------------------|
128
+ | `typeof fn === 'function'` | It's callable |
129
+ | `fn.__tjs` exists | It's a TJS-typed function |
130
+ | Param count matches | Same arity (or compatible) |
131
+ | Param types match | Each param's type descriptor matches |
132
+ | Return type matches | Return type descriptor matches |
133
+ | Return contract | At least as strict as required |
134
+
135
+ Return contract strictness: `checkedReturns` (-?) > `returns` (->) > `assertReturns` (-!).
136
+ A `checkedReturns` function satisfies any requirement.
137
+ A `returns` function satisfies `returns` or `assertReturns`.
138
+ An `assertReturns` function only satisfies `assertReturns`.
139
+
140
+ ## Compatibility with Untyped Functions
141
+
142
+ Plain JS functions have no `__tjs` metadata. Options:
143
+
144
+ 1. **Strict**: Reject untyped functions (safe but hostile to JS interop)
145
+ 2. **Lenient**: Accept any function, only validate if `__tjs` exists
146
+ 3. **Unsafe marker**: Use `!` to skip the check for known-untyped callbacks
147
+
148
+ Option 3 is most consistent with TJS's existing patterns:
149
+
150
+ ```tjs
151
+ // Strict — fn must have matching __tjs metadata
152
+ function process(fn: formatter) { ... }
153
+
154
+ // Lenient — fn just needs to be callable
155
+ function process(! fn: formatter) { ... }
156
+ ```
157
+
158
+ ## Relationship to Existing Features
159
+
160
+ - **Type**: FunctionPredicate IS a Type — just one that checks function
161
+ signatures specifically. Could be implemented as a special case of Type
162
+ with a built-in predicate that introspects `__tjs`.
163
+ - **Generic**: FunctionPredicate could be generic too —
164
+ `FunctionPredicate Mapper<T, U> { params: { value: T }, returns: U }`
165
+ - **declaration block**: FunctionPredicates would benefit from declaration
166
+ blocks for `.d.ts` emission, same as Generic.
167
+
168
+ ## Implementation Path
169
+
170
+ 1. **Runtime**: Add `FunctionPredicate()` to the TJS runtime alongside
171
+ `Type()` and `Generic()`. Returns a type guard that checks `__tjs`
172
+ metadata on functions.
173
+ 2. **Parser**: Recognize `FunctionPredicate` as a declaration keyword
174
+ (same as `Type`, `Generic`). Parse the block or function-argument form.
175
+ 3. **Metadata**: The `__tjs` metadata for the return type already includes
176
+ `type` — add a `contract` field for the marker.
177
+ 4. **fromTS**: When converting `(x: number) => string` types, emit a
178
+ FunctionPredicate instead of `undefined`.
179
+ 5. **Inference**: When a function is used as a `:` param type, check if
180
+ it has `__tjs` metadata and validate the caller's function against it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "Type-safe JavaScript dialect with runtime validation, sandboxed VM execution, and AI agent orchestration. Transpiles TypeScript to validated JS with fuel-metered execution for untrusted code.",
5
5
  "keywords": [
6
6
  "typescript",
package/src/cli/tjs.ts CHANGED
@@ -20,7 +20,7 @@ import { emit } from './commands/emit'
20
20
  import { convert } from './commands/convert'
21
21
  import { test } from './commands/test'
22
22
 
23
- const VERSION = '0.6.13'
23
+ const VERSION = '0.6.15'
24
24
 
25
25
  const HELP = `
26
26
  tjs - Typed JavaScript CLI
@@ -437,6 +437,64 @@ export Generic Box<T> {
437
437
  })
438
438
  })
439
439
 
440
+ describe('generateDTS — FunctionPredicate declarations', () => {
441
+ it('should emit FunctionPredicate as TS function type', () => {
442
+ const source = `
443
+ export FunctionPredicate Callback {
444
+ params: { x: 0, y: '' }
445
+ returns: false
446
+ }
447
+ `
448
+ const result = transpileToJS(source, { runTests: false })
449
+ const dts = generateDTS(result, source)
450
+
451
+ expect(dts).toContain(
452
+ 'export type Callback = (x: number, y: string) => boolean;'
453
+ )
454
+ })
455
+
456
+ it('should emit FunctionPredicate with no params as zero-arg function', () => {
457
+ const source = `
458
+ export FunctionPredicate Thunk {
459
+ returns: 0
460
+ }
461
+ `
462
+ const result = transpileToJS(source, { runTests: false })
463
+ const dts = generateDTS(result, source)
464
+
465
+ expect(dts).toContain('export type Thunk = () => number;')
466
+ })
467
+
468
+ it('should emit FunctionPredicate with no return as void', () => {
469
+ const source = `
470
+ export FunctionPredicate SideEffect {
471
+ params: { msg: '' }
472
+ }
473
+ `
474
+ const result = transpileToJS(source, { runTests: false })
475
+ const dts = generateDTS(result, source)
476
+
477
+ expect(dts).toContain('export type SideEffect = (msg: string) => void;')
478
+ })
479
+
480
+ it('should not emit non-exported FunctionPredicate when exports exist', () => {
481
+ const source = `
482
+ FunctionPredicate Internal {
483
+ params: { x: 0 }
484
+ }
485
+
486
+ export function use(fn: Internal) {
487
+ return fn(1)
488
+ }
489
+ `
490
+ const result = transpileToJS(source, { runTests: false })
491
+ const dts = generateDTS(result, source)
492
+
493
+ expect(dts).not.toContain('type Internal')
494
+ expect(dts).toContain('export declare function use(')
495
+ })
496
+ })
497
+
440
498
  describe('generateDTS — mixed declarations', () => {
441
499
  it('should handle file with functions, classes, types, and generics', () => {
442
500
  const source = `
@@ -169,6 +169,12 @@ function detectExports(source: string): Map<string, ExportInfo> {
169
169
  result.set(m[1], { exported: true, isDefault: false })
170
170
  }
171
171
 
172
+ // export FunctionPredicate Name
173
+ const fpRe = /^[ \t]*export\s+FunctionPredicate\s+(\w+)/gm
174
+ while ((m = fpRe.exec(source)) !== null) {
175
+ result.set(m[1], { exported: true, isDefault: false })
176
+ }
177
+
172
178
  // export { Name, Name2, ... } — re-export form
173
179
  const reExportRe = /^[ \t]*export\s*\{([^}]+)\}/gm
174
180
  while ((m = reExportRe.exec(source)) !== null) {
@@ -184,6 +190,62 @@ function detectExports(source: string): Map<string, ExportInfo> {
184
190
  return result
185
191
  }
186
192
 
193
+ /** Info about a FunctionPredicate declaration */
194
+ interface FunctionPredicateInfo {
195
+ params: { name: string; example: string }[]
196
+ returns?: string
197
+ }
198
+
199
+ /** Detect FunctionPredicate declarations and extract their param/return specs */
200
+ function detectFunctionPredicates(
201
+ source: string
202
+ ): Map<string, FunctionPredicateInfo> {
203
+ const result = new Map<string, FunctionPredicateInfo>()
204
+
205
+ // Block form: FunctionPredicate Name { params: { ... } returns: ... }
206
+ const blockRe = /^[ \t]*(?:export\s+)?FunctionPredicate\s+(\w+)\s*\{/gm
207
+ let m
208
+ while ((m = blockRe.exec(source)) !== null) {
209
+ const name = m[1]
210
+ const blockStart = m.index + m[0].length - 1
211
+
212
+ // Find matching closing brace
213
+ let depth = 1
214
+ let i = blockStart + 1
215
+ while (i < source.length && depth > 0) {
216
+ if (source[i] === '{') depth++
217
+ else if (source[i] === '}') depth--
218
+ i++
219
+ }
220
+ const body = source.slice(blockStart + 1, i - 1)
221
+
222
+ // Extract params object: params: { key: value, ... }
223
+ const params: FunctionPredicateInfo['params'] = []
224
+ const paramsMatch = body.match(/params\s*:\s*\{([^}]*)\}/)
225
+ if (paramsMatch) {
226
+ const paramsStr = paramsMatch[1]
227
+ const paramEntries = splitParams(paramsStr)
228
+ for (const entry of paramEntries) {
229
+ const kv = entry.match(/^(\w+)\s*:\s*(.+)$/)
230
+ if (kv) {
231
+ params.push({ name: kv[1], example: kv[2].trim() })
232
+ }
233
+ }
234
+ }
235
+
236
+ // Extract returns value
237
+ let returns: string | undefined
238
+ const returnsMatch = body.match(/returns\s*:\s*(.+?)(?:\n|$)/)
239
+ if (returnsMatch) {
240
+ returns = returnsMatch[1].trim()
241
+ }
242
+
243
+ result.set(name, { params, returns })
244
+ }
245
+
246
+ return result
247
+ }
248
+
187
249
  /** Info about a class extracted from source */
188
250
  interface ClassInfo {
189
251
  name: string
@@ -535,6 +597,28 @@ export function generateDTS(
535
597
  emitted.add(name)
536
598
  }
537
599
 
600
+ // Emit FunctionPredicate declarations as TS function types.
601
+ // FunctionPredicate Callback { params: { x: 0 } returns: '' }
602
+ // → export type Callback = (x: number) => string;
603
+ const funcPreds = detectFunctionPredicates(source)
604
+ for (const [name, fpInfo] of funcPreds) {
605
+ if (emitted.has(name)) continue
606
+
607
+ const exportInfo = exports.get(name)
608
+ const isExported = hasAnyExport ? !!exportInfo?.exported : true
609
+ if (!isExported) continue
610
+
611
+ const tsParams = fpInfo.params
612
+ .map((p) => `${p.name}: ${inferTSTypeFromExample(p.example)}`)
613
+ .join(', ')
614
+ const tsReturn =
615
+ fpInfo.returns !== undefined
616
+ ? inferTSTypeFromExample(fpInfo.returns)
617
+ : 'void'
618
+ lines.push(`export type ${name} = (${tsParams}) => ${tsReturn};`)
619
+ emitted.add(name)
620
+ }
621
+
538
622
  if (options.moduleName) {
539
623
  const indented = lines.map((l) => ` ${l}`).join('\n')
540
624
  return `declare module '${options.moduleName}' {\n${indented}\n}\n`
@@ -124,6 +124,158 @@ interface TypeResolutionContext {
124
124
  typeParams?: Map<string, { constraint?: ts.TypeNode; default?: ts.TypeNode }>
125
125
  }
126
126
 
127
+ /**
128
+ * DOM interface types — not constructible but common in TS signatures.
129
+ * Map to {} (opaque object) so params stay annotated and required
130
+ * rather than degrading to bare names.
131
+ */
132
+ const domInterfaceTypes = new Set([
133
+ // Events
134
+ 'Event',
135
+ 'CustomEvent',
136
+ 'MouseEvent',
137
+ 'KeyboardEvent',
138
+ 'PointerEvent',
139
+ 'TouchEvent',
140
+ 'FocusEvent',
141
+ 'InputEvent',
142
+ 'CompositionEvent',
143
+ 'WheelEvent',
144
+ 'DragEvent',
145
+ 'AnimationEvent',
146
+ 'TransitionEvent',
147
+ 'ClipboardEvent',
148
+ 'UIEvent',
149
+ 'ProgressEvent',
150
+ 'ErrorEvent',
151
+ 'MessageEvent',
152
+ 'PopStateEvent',
153
+ 'HashChangeEvent',
154
+ 'PageTransitionEvent',
155
+ 'StorageEvent',
156
+ 'BeforeUnloadEvent',
157
+ 'SubmitEvent',
158
+ // Event targets / misc
159
+ 'EventTarget',
160
+ 'EventListener',
161
+ // Nodes
162
+ 'Node',
163
+ 'Element',
164
+ 'HTMLElement',
165
+ 'SVGElement',
166
+ 'Document',
167
+ 'DocumentFragment',
168
+ 'ShadowRoot',
169
+ 'Text',
170
+ 'Comment',
171
+ 'Attr',
172
+ // Specific HTML elements
173
+ 'HTMLInputElement',
174
+ 'HTMLTextAreaElement',
175
+ 'HTMLSelectElement',
176
+ 'HTMLButtonElement',
177
+ 'HTMLFormElement',
178
+ 'HTMLAnchorElement',
179
+ 'HTMLImageElement',
180
+ 'HTMLVideoElement',
181
+ 'HTMLAudioElement',
182
+ 'HTMLCanvasElement',
183
+ 'HTMLDivElement',
184
+ 'HTMLSpanElement',
185
+ 'HTMLParagraphElement',
186
+ 'HTMLTableElement',
187
+ 'HTMLTemplateElement',
188
+ 'HTMLSlotElement',
189
+ 'HTMLDialogElement',
190
+ 'HTMLDetailsElement',
191
+ 'HTMLLabelElement',
192
+ 'HTMLOptionElement',
193
+ 'HTMLIFrameElement',
194
+ 'HTMLScriptElement',
195
+ 'HTMLStyleElement',
196
+ 'HTMLLinkElement',
197
+ 'HTMLMetaElement',
198
+ 'HTMLHeadElement',
199
+ 'HTMLBodyElement',
200
+ 'HTMLMediaElement',
201
+ // SVG elements
202
+ 'SVGSVGElement',
203
+ 'SVGPathElement',
204
+ 'SVGGElement',
205
+ 'SVGCircleElement',
206
+ 'SVGRectElement',
207
+ 'SVGTextElement',
208
+ 'SVGLineElement',
209
+ 'SVGPolygonElement',
210
+ // Collections / lists
211
+ 'NodeList',
212
+ 'HTMLCollection',
213
+ 'NamedNodeMap',
214
+ 'DOMTokenList',
215
+ 'DOMStringMap',
216
+ 'CSSStyleDeclaration',
217
+ 'DOMRect',
218
+ 'DOMRectReadOnly',
219
+ 'DOMPoint',
220
+ 'DOMMatrix',
221
+ // Ranges / selection
222
+ 'Range',
223
+ 'Selection',
224
+ 'StaticRange',
225
+ // Observers
226
+ 'MutationObserver',
227
+ 'MutationRecord',
228
+ 'IntersectionObserver',
229
+ 'IntersectionObserverEntry',
230
+ 'ResizeObserver',
231
+ 'ResizeObserverEntry',
232
+ 'PerformanceObserver',
233
+ 'PerformanceEntry',
234
+ // Window / global
235
+ 'Window',
236
+ 'Location',
237
+ 'History',
238
+ 'Navigator',
239
+ 'Screen',
240
+ 'Storage',
241
+ // Canvas / media
242
+ 'CanvasRenderingContext2D',
243
+ 'WebGLRenderingContext',
244
+ 'WebGL2RenderingContext',
245
+ 'OffscreenCanvas',
246
+ 'ImageData',
247
+ 'ImageBitmap',
248
+ 'MediaStream',
249
+ 'MediaRecorder',
250
+ 'AudioContext',
251
+ 'AudioNode',
252
+ 'AudioBuffer',
253
+ // Workers / messaging
254
+ 'Worker',
255
+ 'SharedWorker',
256
+ 'ServiceWorker',
257
+ 'ServiceWorkerRegistration',
258
+ 'BroadcastChannel',
259
+ 'MessageChannel',
260
+ 'MessagePort',
261
+ // Other Web APIs
262
+ 'WebSocket',
263
+ 'XMLHttpRequest',
264
+ 'FileReader',
265
+ 'FileList',
266
+ 'DataTransfer',
267
+ 'Crypto',
268
+ 'SubtleCrypto',
269
+ 'CryptoKey',
270
+ 'Geolocation',
271
+ 'Notification',
272
+ 'PermissionStatus',
273
+ 'MediaQueryList',
274
+ 'TreeWalker',
275
+ 'NodeIterator',
276
+ 'ClipboardItem',
277
+ ])
278
+
127
279
  /**
128
280
  * Convert a TypeScript type node to a TJS example value string
129
281
  *
@@ -221,6 +373,10 @@ function typeToExample(
221
373
  Error: "new Error('example')",
222
374
  TypeError: "new TypeError('example')",
223
375
  RangeError: "new RangeError('example')",
376
+ SyntaxError: "new SyntaxError('example')",
377
+ ReferenceError: "new ReferenceError('example')",
378
+ URIError: "new URIError('example')",
379
+ EvalError: "new EvalError('example')",
224
380
  // Date/Regex
225
381
  Date: 'new Date()',
226
382
  RegExp: '/example/',
@@ -239,7 +395,7 @@ function typeToExample(
239
395
  Uint8ClampedArray: 'new Uint8ClampedArray(0)',
240
396
  BigInt64Array: 'new BigInt64Array(0)',
241
397
  BigUint64Array: 'new BigUint64Array(0)',
242
- // Web/DOM
398
+ // Web APIs (constructible)
243
399
  URL: "new URL('https://example.com')",
244
400
  URLSearchParams: 'new URLSearchParams()',
245
401
  Headers: 'new Headers()',
@@ -249,6 +405,7 @@ function typeToExample(
249
405
  Response: 'new Response()',
250
406
  Request: "new Request('https://example.com')",
251
407
  AbortController: 'new AbortController()',
408
+ AbortSignal: 'AbortSignal.abort()',
252
409
  // Streams
253
410
  ReadableStream: 'new ReadableStream()',
254
411
  WritableStream: 'new WritableStream()',
@@ -256,6 +413,8 @@ function typeToExample(
256
413
  // Structured data
257
414
  TextEncoder: 'new TextEncoder()',
258
415
  TextDecoder: 'new TextDecoder()',
416
+ // Promises
417
+ Promise: 'Promise.resolve(null)',
259
418
  }
260
419
 
261
420
  if (typeName in builtinExamples) {
@@ -318,6 +477,11 @@ function typeToExample(
318
477
  // No constraint or default — fall through to 'any'
319
478
  }
320
479
 
480
+ // DOM interface types — opaque objects, keep params annotated
481
+ if (domInterfaceTypes.has(typeName)) {
482
+ return '{}'
483
+ }
484
+
321
485
  // Single uppercase letter or common generic names — treat as any
322
486
  if (
323
487
  /^[A-Z]$/.test(typeName) ||
@@ -420,9 +584,24 @@ function typeToExample(
420
584
  return typeToExample(parenType.type, checker)
421
585
  }
422
586
 
423
- case ts.SyntaxKind.FunctionType:
424
- // Functions become undefined (can't really express as example)
425
- return 'undefined'
587
+ case ts.SyntaxKind.FunctionType: {
588
+ // Convert to inline FunctionPredicate expression
589
+ const funcType = type as ts.FunctionTypeNode
590
+ const fpParams: string[] = []
591
+ for (const param of funcType.parameters) {
592
+ const name = param.name?.getText() || '_'
593
+ if (name === 'this') continue
594
+ let paramExample = typeToExample(param.type, checker, warnings, ctx)
595
+ if (paramExample === 'any') paramExample = 'null'
596
+ fpParams.push(`${name}: ${paramExample}`)
597
+ }
598
+ let fpReturn = typeToExample(funcType.type, checker, warnings, ctx)
599
+ if (fpReturn === 'any') fpReturn = 'null'
600
+ const spec: string[] = []
601
+ if (fpParams.length > 0) spec.push(`params: { ${fpParams.join(', ')} }`)
602
+ if (fpReturn !== 'undefined') spec.push(`returns: ${fpReturn}`)
603
+ return `FunctionPredicate('function', { ${spec.join(', ')} })`
604
+ }
426
605
 
427
606
  case ts.SyntaxKind.TupleType: {
428
607
  const tupleType = type as ts.TupleTypeNode
@@ -669,6 +848,11 @@ function typeToInfo(
669
848
  }
670
849
  }
671
850
 
851
+ // DOM interface types — opaque objects
852
+ if (domInterfaceTypes.has(typeName)) {
853
+ return { kind: 'object' }
854
+ }
855
+
672
856
  // Generics and unknown types become 'any'
673
857
  return { kind: 'any' }
674
858
  }
@@ -978,6 +1162,25 @@ function transformTypeAliasToType(
978
1162
  return `Union ${typeName} '${typeName}' ${literalValues.join(' | ')}`
979
1163
  }
980
1164
 
1165
+ // Function types → FunctionPredicate declaration
1166
+ if (node.type.kind === ts.SyntaxKind.FunctionType) {
1167
+ const funcType = node.type as ts.FunctionTypeNode
1168
+ const fpParams: string[] = []
1169
+ for (const param of funcType.parameters) {
1170
+ const name = param.name?.getText(sourceFile) || '_'
1171
+ if (name === 'this') continue
1172
+ let paramExample = typeToExample(param.type, undefined, warnings)
1173
+ if (paramExample === 'any') paramExample = 'null'
1174
+ fpParams.push(`${name}: ${paramExample}`)
1175
+ }
1176
+ let fpReturn = typeToExample(funcType.type, undefined, warnings)
1177
+ if (fpReturn === 'any') fpReturn = 'null'
1178
+ const spec: string[] = []
1179
+ if (fpParams.length > 0) spec.push(`params: { ${fpParams.join(', ')} }`)
1180
+ if (fpReturn !== 'undefined') spec.push(`returns: ${fpReturn}`)
1181
+ return `FunctionPredicate ${typeName} {\n ${spec.join('\n ')}\n}`
1182
+ }
1183
+
981
1184
  const example = typeToExample(node.type, undefined, warnings)
982
1185
 
983
1186
  // 'any' and 'undefined' — skip declaration (undeclared = any in TJS)
@@ -2054,7 +2257,11 @@ export function fromTS(
2054
2257
  const isExported = statement.modifiers?.some(
2055
2258
  (m) => m.kind === ts.SyntaxKind.ExportKeyword
2056
2259
  )
2057
- tjsFunctions.push(isExported ? `export ${typeDecl}` : typeDecl)
2260
+ tjsFunctions.push(
2261
+ isExported
2262
+ ? typeDecl.replace(/^(\/\*[\s\S]*?\*\/\s*)?/, '$1export ')
2263
+ : typeDecl
2264
+ )
2058
2265
  }
2059
2266
  }
2060
2267
  }
@@ -2076,7 +2283,11 @@ export function fromTS(
2076
2283
  const isExported = statement.modifiers?.some(
2077
2284
  (m) => m.kind === ts.SyntaxKind.ExportKeyword
2078
2285
  )
2079
- tjsFunctions.push(isExported ? `export ${typeDecl}` : typeDecl)
2286
+ tjsFunctions.push(
2287
+ isExported
2288
+ ? typeDecl.replace(/^(\/\*[\s\S]*?\*\/\s*)?/, '$1export ')
2289
+ : typeDecl
2290
+ )
2080
2291
  }
2081
2292
  }
2082
2293
  }