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,188 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { FunctionPredicate } from '../types/Type'
3
+ import { preprocess } from './parser'
4
+ import { tjs } from './index'
5
+ import { fromTS } from './emitters/from-ts'
6
+
7
+ describe('FunctionPredicate runtime', () => {
8
+ it('should create a type that accepts functions', () => {
9
+ const Callback = FunctionPredicate('Callback', {
10
+ params: { x: 0 },
11
+ returns: '',
12
+ })
13
+ expect(Callback.check(() => {})).toBe(true)
14
+ expect(Callback.check((x: number) => String(x))).toBe(true)
15
+ expect(Callback.check(42)).toBe(false)
16
+ expect(Callback.check('not a function')).toBe(false)
17
+ expect(Callback.check(null)).toBe(false)
18
+ })
19
+
20
+ it('should create from existing typed function', () => {
21
+ const fn = (a: number, b: number) => a + b
22
+ ;(fn as any).__tjs = {
23
+ params: {
24
+ a: { type: { kind: 'integer' }, example: 0 },
25
+ b: { type: { kind: 'integer' }, example: 0 },
26
+ },
27
+ returns: { type: { kind: 'integer' }, example: 0 },
28
+ }
29
+ const Adder = FunctionPredicate('Adder', fn)
30
+ expect(Adder.params).toHaveProperty('a')
31
+ expect(Adder.params).toHaveProperty('b')
32
+ expect(Adder.description).toBe('Adder')
33
+ expect(Adder.__runtimeType).toBe(true)
34
+ })
35
+
36
+ it('should have correct return contract', () => {
37
+ const assert = FunctionPredicate('f', {
38
+ returnContract: 'assertReturns',
39
+ })
40
+ const returns = FunctionPredicate('f', { returnContract: 'returns' })
41
+ const checked = FunctionPredicate('f', {
42
+ returnContract: 'checkedReturns',
43
+ })
44
+ expect(assert.returnContract).toBe('assertReturns')
45
+ expect(returns.returnContract).toBe('returns')
46
+ expect(checked.returnContract).toBe('checkedReturns')
47
+ })
48
+
49
+ it('should default to assertReturns contract', () => {
50
+ const fp = FunctionPredicate('f', {})
51
+ expect(fp.returnContract).toBe('assertReturns')
52
+ })
53
+
54
+ it('should reject function with wrong arity via __tjs metadata', () => {
55
+ const Binop = FunctionPredicate('Binop', {
56
+ params: { a: 0, b: 0 },
57
+ returns: 0,
58
+ })
59
+ // Function with matching arity + types → pass
60
+ const goodFn = (a: number, b: number) => a + b
61
+ ;(goodFn as any).__tjs = {
62
+ params: {
63
+ a: { type: { kind: 'integer' }, example: 0 },
64
+ b: { type: { kind: 'integer' }, example: 0 },
65
+ },
66
+ }
67
+ expect(Binop.check(goodFn)).toBe(true)
68
+
69
+ // Function with wrong arity → fail
70
+ const wrongArity = (x: number) => x
71
+ ;(wrongArity as any).__tjs = {
72
+ params: {
73
+ x: { type: { kind: 'integer' }, example: 0 },
74
+ },
75
+ }
76
+ expect(Binop.check(wrongArity)).toBe(false)
77
+ })
78
+
79
+ it('should reject function with wrong param types via __tjs metadata', () => {
80
+ const StrFn = FunctionPredicate('StrFn', {
81
+ params: { name: '' },
82
+ returns: '',
83
+ })
84
+ // Function with string param → pass
85
+ const goodFn = (s: string) => s
86
+ ;(goodFn as any).__tjs = {
87
+ params: { s: { type: { kind: 'string' }, example: '' } },
88
+ }
89
+ expect(StrFn.check(goodFn)).toBe(true)
90
+
91
+ // Function with number param → fail
92
+ const badFn = (n: number) => String(n)
93
+ ;(badFn as any).__tjs = {
94
+ params: { n: { type: { kind: 'integer' }, example: 0 } },
95
+ }
96
+ expect(StrFn.check(badFn)).toBe(false)
97
+ })
98
+
99
+ it('should accept function with any-typed params', () => {
100
+ const Binop = FunctionPredicate('Binop', {
101
+ params: { a: 0, b: 0 },
102
+ })
103
+ const fn = (a: any, b: any) => a + b
104
+ ;(fn as any).__tjs = {
105
+ params: {
106
+ a: { type: { kind: 'any' }, example: null },
107
+ b: { type: { kind: 'any' }, example: null },
108
+ },
109
+ }
110
+ expect(Binop.check(fn)).toBe(true)
111
+ })
112
+
113
+ it('should accept plain functions without __tjs metadata', () => {
114
+ const Callback = FunctionPredicate('Callback', {
115
+ params: { x: 0 },
116
+ })
117
+ // Plain function with no metadata — should still pass (can't validate)
118
+ expect(Callback.check(() => {})).toBe(true)
119
+ expect(Callback.check(Math.abs)).toBe(true)
120
+ })
121
+ })
122
+
123
+ describe('FunctionPredicate parser transform', () => {
124
+ it('should transform block form', () => {
125
+ const result = preprocess(
126
+ "FunctionPredicate Callback {\n params: { x: 0 }\n returns: ''\n}"
127
+ )
128
+ expect(result.source).toContain("FunctionPredicate('Callback'")
129
+ expect(result.source).toContain('params: { x: 0 }')
130
+ expect(result.source).toContain("returns: ''")
131
+ })
132
+
133
+ it('should transform function form', () => {
134
+ const result = preprocess(
135
+ "FunctionPredicate Handler(myFn, 'event handler')"
136
+ )
137
+ expect(result.source).toContain("FunctionPredicate('event handler'")
138
+ expect(result.source).toContain('myFn')
139
+ })
140
+
141
+ it('should transpile block form through full pipeline', () => {
142
+ const result = tjs(
143
+ 'FunctionPredicate Callback {\n params: { x: 0 }\n returns: false\n}',
144
+ { runTests: false }
145
+ )
146
+ expect(result.code).toContain('FunctionPredicate')
147
+ expect(result.code).toContain('Callback')
148
+ })
149
+ })
150
+
151
+ describe('FunctionPredicate in fromTS', () => {
152
+ it('should convert function type alias to FunctionPredicate', () => {
153
+ const result = fromTS('type Callback = (x: number) => void', {
154
+ emitTJS: true,
155
+ })
156
+ expect(result.code).toContain('FunctionPredicate Callback')
157
+ expect(result.code).toContain('params: { x: 0.0 }')
158
+ })
159
+
160
+ it('should convert function type with return to FunctionPredicate', () => {
161
+ const result = fromTS('type Mapper = (value: string) => number', {
162
+ emitTJS: true,
163
+ })
164
+ expect(result.code).toContain('FunctionPredicate Mapper')
165
+ expect(result.code).toContain("params: { value: '' }")
166
+ expect(result.code).toContain('returns: 0.0')
167
+ })
168
+
169
+ it('should handle inline function params', () => {
170
+ const result = fromTS(
171
+ 'function process(cb: (x: number) => string): void {}',
172
+ { emitTJS: true }
173
+ )
174
+ expect(result.code).toContain("FunctionPredicate('function'")
175
+ })
176
+
177
+ it('should handle void function type', () => {
178
+ const result = fromTS('type VoidFn = () => void', { emitTJS: true })
179
+ expect(result.code).toContain('FunctionPredicate VoidFn')
180
+ })
181
+
182
+ it('should preserve export on function type alias', () => {
183
+ const result = fromTS('export type Handler = (event: Event) => boolean', {
184
+ emitTJS: true,
185
+ })
186
+ expect(result.code).toContain('export FunctionPredicate Handler')
187
+ })
188
+ })
@@ -1188,6 +1188,109 @@ export function transformTypeDeclarations(source: string): string {
1188
1188
  return result
1189
1189
  }
1190
1190
 
1191
+ /**
1192
+ * Transform FunctionPredicate declarations
1193
+ *
1194
+ * Block form:
1195
+ * FunctionPredicate Callback {
1196
+ * params: { x: 0, y: '' }
1197
+ * returns: false
1198
+ * }
1199
+ * → const Callback = FunctionPredicate('Callback', { params: { x: 0, y: '' }, returns: false })
1200
+ *
1201
+ * Function form:
1202
+ * FunctionPredicate Handler(existingFn, 'description')
1203
+ * → const Handler = FunctionPredicate('description', existingFn)
1204
+ */
1205
+ export function transformFunctionPredicateDeclarations(source: string): string {
1206
+ let result = ''
1207
+ let i = 0
1208
+
1209
+ while (i < source.length) {
1210
+ const fpMatch = source
1211
+ .slice(i)
1212
+ .match(/^\bFunctionPredicate\s+([A-Z_][a-zA-Z0-9_]*)\s*/)
1213
+ if (fpMatch) {
1214
+ const fpName = fpMatch[1]
1215
+ const j = i + fpMatch[0].length
1216
+
1217
+ // Check for block form: FunctionPredicate Name { ... }
1218
+ if (source[j] === '{') {
1219
+ // Find matching closing brace
1220
+ let depth = 1
1221
+ let k = j + 1
1222
+ while (k < source.length && depth > 0) {
1223
+ if (source[k] === '{') depth++
1224
+ else if (source[k] === '}') depth--
1225
+ k++
1226
+ }
1227
+
1228
+ if (depth === 0) {
1229
+ const blockBody = source.slice(j + 1, k - 1).trim()
1230
+
1231
+ // Extract params: { ... }
1232
+ const paramsMatch = blockBody.match(/params\s*:\s*(\{[^}]*\})/)
1233
+ // Extract returns value
1234
+ const returnsMatch = blockBody.match(/returns\s*:\s*(.+?)(?:\n|$)/)
1235
+ // Extract returnContract
1236
+ const contractMatch = blockBody.match(
1237
+ /returnContract\s*:\s*['"](\w+)['"]/
1238
+ )
1239
+ // Extract description
1240
+ const descMatch = blockBody.match(/description\s*:\s*(['"])([^]*?)\1/)
1241
+
1242
+ const spec: string[] = []
1243
+ if (paramsMatch) spec.push(`params: ${paramsMatch[1]}`)
1244
+ if (returnsMatch) spec.push(`returns: ${returnsMatch[1].trim()}`)
1245
+ if (contractMatch) {
1246
+ spec.push(`returnContract: '${contractMatch[1]}'`)
1247
+ }
1248
+
1249
+ const desc = descMatch ? descMatch[2] : fpName
1250
+ result += `const ${fpName} = FunctionPredicate('${desc}', { ${spec.join(
1251
+ ', '
1252
+ )} })`
1253
+ i = k
1254
+ continue
1255
+ }
1256
+ }
1257
+
1258
+ // Check for function form: FunctionPredicate Name(fn, 'desc')
1259
+ if (source[j] === '(') {
1260
+ // Find matching closing paren
1261
+ let depth = 1
1262
+ let k = j + 1
1263
+ while (k < source.length && depth > 0) {
1264
+ if (source[k] === '(') depth++
1265
+ else if (source[k] === ')') depth--
1266
+ k++
1267
+ }
1268
+
1269
+ if (depth === 0) {
1270
+ const args = source.slice(j + 1, k - 1).trim()
1271
+ // Split on comma: fn, 'description'
1272
+ const commaIdx = args.indexOf(',')
1273
+ if (commaIdx !== -1) {
1274
+ const fnRef = args.slice(0, commaIdx).trim()
1275
+ const desc = args.slice(commaIdx + 1).trim()
1276
+ result += `const ${fpName} = FunctionPredicate(${desc}, ${fnRef})`
1277
+ } else {
1278
+ // Just a function reference, name as description
1279
+ result += `const ${fpName} = FunctionPredicate('${fpName}', ${args})`
1280
+ }
1281
+ i = k
1282
+ continue
1283
+ }
1284
+ }
1285
+ }
1286
+
1287
+ result += source[i]
1288
+ i++
1289
+ }
1290
+
1291
+ return result
1292
+ }
1293
+
1191
1294
  /**
1192
1295
  * Transform Generic block declarations
1193
1296
  *
@@ -36,6 +36,7 @@ import {
36
36
  transformEqualityToStructural,
37
37
  transformTypeDeclarations,
38
38
  transformGenericDeclarations,
39
+ transformFunctionPredicateDeclarations,
39
40
  transformUnionDeclarations,
40
41
  transformEnumDeclarations,
41
42
  transformExtendDeclarations,
@@ -167,6 +168,7 @@ export function preprocess(
167
168
  // Enum Status { Pending, Active, Done } -> const Status = Enum(...)
168
169
  source = transformTypeDeclarations(source)
169
170
  source = transformGenericDeclarations(source)
171
+ source = transformFunctionPredicateDeclarations(source)
170
172
  source = transformUnionDeclarations(source)
171
173
  source = transformEnumDeclarations(source)
172
174
 
@@ -17,6 +17,7 @@ import {
17
17
  Union,
18
18
  Generic,
19
19
  Enum,
20
+ FunctionPredicate,
20
21
  Nullable,
21
22
  Optional,
22
23
  TArray,
@@ -48,6 +49,7 @@ export {
48
49
  Union,
49
50
  Generic,
50
51
  Enum,
52
+ FunctionPredicate,
51
53
  Nullable,
52
54
  Optional,
53
55
  TArray,
@@ -1179,6 +1181,7 @@ export function createRuntime() {
1179
1181
  Union,
1180
1182
  Generic,
1181
1183
  Enum,
1184
+ FunctionPredicate,
1182
1185
  Nullable,
1183
1186
  Optional,
1184
1187
  TArray,
@@ -1251,6 +1254,7 @@ export const runtime = {
1251
1254
  Union,
1252
1255
  Generic,
1253
1256
  Enum,
1257
+ FunctionPredicate,
1254
1258
  Nullable,
1255
1259
  Optional,
1256
1260
  TArray,
@@ -1312,10 +1312,9 @@ describe('Constrained generics use constraint as example', () => {
1312
1312
  // =============================================================================
1313
1313
 
1314
1314
  describe('fromTS — tosijs conversion edge cases', () => {
1315
- test('unknown type param before known type param does not error', () => {
1316
- // When a param type is unknown (e.g. Element), it degrades to bare name
1317
- // (looks optional). A subsequent typed param should not cause
1318
- // "required param after optional" error.
1315
+ test('DOM type params stay annotated (not degraded to bare name)', () => {
1316
+ // Element is a DOM interface type should map to {} (opaque object)
1317
+ // so the param stays annotated and required, not degraded to bare name
1319
1318
  const result = fromTS(
1320
1319
  `export function touchElement(element: Element, changedPath?: string): void {
1321
1320
  console.log(element, changedPath)
@@ -1323,7 +1322,8 @@ describe('fromTS — tosijs conversion edge cases', () => {
1323
1322
  { emitTJS: true }
1324
1323
  )
1325
1324
  expect(result.code).toContain('function touchElement(')
1326
- // Should succeed without error
1325
+ // Element should be annotated with {} (opaque object), not bare name
1326
+ expect(result.code).toContain('element: {}')
1327
1327
  expect(result.code).toBeDefined()
1328
1328
  })
1329
1329
 
@@ -1594,6 +1594,155 @@ describe('fromTS — tosijs conversion edge cases', () => {
1594
1594
  })
1595
1595
  })
1596
1596
 
1597
+ // =============================================================================
1598
+ // DOM TYPES
1599
+ // =============================================================================
1600
+
1601
+ describe('DOM Types', () => {
1602
+ test('Event param maps to opaque object', () => {
1603
+ const result = fromTS(`function handle(e: Event): void {}`, {
1604
+ emitTJS: true,
1605
+ })
1606
+ expect(result.code).toContain('e: {}')
1607
+ // Should NOT have degraded comment
1608
+ expect(result.code).not.toContain('TODO: TS types degraded')
1609
+ })
1610
+
1611
+ test('HTMLElement param maps to opaque object', () => {
1612
+ const result = fromTS(`function render(el: HTMLElement): void {}`, {
1613
+ emitTJS: true,
1614
+ })
1615
+ expect(result.code).toContain('el: {}')
1616
+ })
1617
+
1618
+ test('specific HTML element types map to opaque object', () => {
1619
+ const result = fromTS(
1620
+ `function setup(input: HTMLInputElement, form: HTMLFormElement): void {}`,
1621
+ { emitTJS: true }
1622
+ )
1623
+ expect(result.code).toContain('input: {}')
1624
+ expect(result.code).toContain('form: {}')
1625
+ })
1626
+
1627
+ test('MouseEvent param maps to opaque object', () => {
1628
+ const result = fromTS(
1629
+ `function onClick(ev: MouseEvent): boolean { return true }`,
1630
+ { emitTJS: true }
1631
+ )
1632
+ expect(result.code).toContain('ev: {}')
1633
+ })
1634
+
1635
+ test('Document and Node params map to opaque object', () => {
1636
+ const result = fromTS(
1637
+ `function traverse(doc: Document, node: Node): void {}`,
1638
+ { emitTJS: true }
1639
+ )
1640
+ expect(result.code).toContain('doc: {}')
1641
+ expect(result.code).toContain('node: {}')
1642
+ })
1643
+
1644
+ test('DOM types as return types', () => {
1645
+ const result = fromTS(
1646
+ `function getRoot(): HTMLElement { return document.body }`,
1647
+ { emitTJS: true }
1648
+ )
1649
+ // Return type should be {} not degraded
1650
+ expect(result.code).toContain('-! {}')
1651
+ })
1652
+
1653
+ test('DOM callback type preserves annotation', () => {
1654
+ const result = fromTS(`type ClickHandler = (ev: MouseEvent) => void`, {
1655
+ emitTJS: true,
1656
+ })
1657
+ expect(result.code).toContain('FunctionPredicate ClickHandler')
1658
+ })
1659
+
1660
+ test('mixed DOM and primitive params', () => {
1661
+ const result = fromTS(
1662
+ `function bind(el: Element, attr: string, value: number): void {}`,
1663
+ { emitTJS: true }
1664
+ )
1665
+ expect(result.code).toContain('el: {}')
1666
+ expect(result.code).toContain("attr: ''")
1667
+ expect(result.code).toContain('value: 0.0')
1668
+ })
1669
+
1670
+ test('DOM types in metadata mode', () => {
1671
+ const { types } = fromTS(`function handle(e: Event): void {}`)
1672
+ // Should be 'object' not 'any'
1673
+ expect(types?.handle.params.e.type.kind).toBe('object')
1674
+ })
1675
+ })
1676
+
1677
+ // =============================================================================
1678
+ // FUNCTION TYPES → FunctionPredicate
1679
+ // =============================================================================
1680
+
1681
+ describe('Function Types (FunctionPredicate)', () => {
1682
+ test('function type alias emits FunctionPredicate', () => {
1683
+ const result = fromTS(`type Callback = (x: number, y: string) => boolean`, {
1684
+ emitTJS: true,
1685
+ })
1686
+ expect(result.code).toContain('FunctionPredicate Callback')
1687
+ expect(result.code).toContain('params: { x: 0.0')
1688
+ expect(result.code).toContain("y: ''")
1689
+ expect(result.code).toContain('returns: false')
1690
+ })
1691
+
1692
+ test('void function type alias', () => {
1693
+ const result = fromTS(`type Logger = (msg: string) => void`, {
1694
+ emitTJS: true,
1695
+ })
1696
+ expect(result.code).toContain('FunctionPredicate Logger')
1697
+ expect(result.code).toContain("params: { msg: '' }")
1698
+ // void return should not emit a returns line
1699
+ expect(result.code).not.toMatch(/returns:/)
1700
+ })
1701
+
1702
+ test('exported function type alias preserves export', () => {
1703
+ const result = fromTS(`export type Handler = (event: Event) => boolean`, {
1704
+ emitTJS: true,
1705
+ })
1706
+ expect(result.code).toContain('export FunctionPredicate Handler')
1707
+ })
1708
+
1709
+ test('inline function param emits FunctionPredicate', () => {
1710
+ const result = fromTS(
1711
+ `function process(cb: (item: string) => number): void {}`,
1712
+ { emitTJS: true }
1713
+ )
1714
+ expect(result.code).toContain("FunctionPredicate('function'")
1715
+ })
1716
+
1717
+ test('no-arg function type', () => {
1718
+ const result = fromTS(`type Thunk = () => number`, { emitTJS: true })
1719
+ expect(result.code).toContain('FunctionPredicate Thunk')
1720
+ expect(result.code).toContain('returns: 0.0')
1721
+ })
1722
+
1723
+ test('function type with multiple params', () => {
1724
+ const result = fromTS(
1725
+ `type Reducer = (acc: number, item: string, index: number) => number`,
1726
+ { emitTJS: true }
1727
+ )
1728
+ expect(result.code).toContain('FunctionPredicate Reducer')
1729
+ expect(result.code).toContain('acc: 0.0')
1730
+ expect(result.code).toContain("item: ''")
1731
+ expect(result.code).toContain('index: 0.0')
1732
+ expect(result.code).toContain('returns: 0.0')
1733
+ })
1734
+
1735
+ test('function type transpiles through full TJS pipeline', () => {
1736
+ const tsResult = fromTS(
1737
+ `type Compare = (a: number, b: number) => boolean`,
1738
+ { emitTJS: true }
1739
+ )
1740
+ const jsResult = tjs(tsResult.code, { runTests: false })
1741
+ expect(jsResult.code).toContain('FunctionPredicate')
1742
+ expect(jsResult.code).toContain('Compare')
1743
+ })
1744
+ })
1745
+
1597
1746
  // =============================================================================
1598
1747
  // SUMMARY: Features to implement (in priority order)
1599
1748
  // =============================================================================
package/src/types/Type.ts CHANGED
@@ -608,3 +608,151 @@ export function Enum<T extends Record<string, string | number>>(
608
608
 
609
609
  return enumType
610
610
  }
611
+
612
+ // =============================================================================
613
+ // FunctionPredicate - Runtime type for function signatures
614
+ // =============================================================================
615
+
616
+ /** Return contract levels in order of strictness */
617
+ export type ReturnContract = 'assertReturns' | 'returns' | 'checkedReturns'
618
+
619
+ /** Specification for a FunctionPredicate */
620
+ export interface FunctionPredicateSpec {
621
+ /** Parameter types as example values */
622
+ params?: Record<string, any>
623
+ /** Return type as example value */
624
+ returns?: any
625
+ /** Return contract level */
626
+ returnContract?: ReturnContract
627
+ }
628
+
629
+ /** A runtime type that validates function signatures */
630
+ // eslint-disable-next-line @typescript-eslint/ban-types
631
+ export interface FunctionPredicateType extends RuntimeType<Function> {
632
+ /** Parameter specification */
633
+ readonly params: Record<string, any>
634
+ /** Return type specification */
635
+ readonly returns?: any
636
+ /** Return contract level */
637
+ readonly returnContract: ReturnContract
638
+ }
639
+
640
+ /** Infer a TypeDescriptor kind from an example value */
641
+ function kindOfExample(example: unknown): string | null {
642
+ if (example === null) return 'null'
643
+ if (example === undefined) return 'undefined'
644
+ switch (typeof example) {
645
+ case 'string':
646
+ return 'string'
647
+ case 'boolean':
648
+ return 'boolean'
649
+ case 'number':
650
+ return Number.isInteger(example) ? 'integer' : 'number'
651
+ case 'object':
652
+ return Array.isArray(example) ? 'array' : 'object'
653
+ default:
654
+ return null
655
+ }
656
+ }
657
+
658
+ /**
659
+ * Create a runtime type for function signatures.
660
+ *
661
+ * Forms:
662
+ * FunctionPredicate(name, spec) - from a specification object
663
+ * FunctionPredicate(name, fn) - from an existing typed function
664
+ *
665
+ * @example
666
+ * const Callback = FunctionPredicate('Callback', {
667
+ * params: { x: 0, y: 0 },
668
+ * returns: 0,
669
+ * })
670
+ * Callback.check((a, b) => a + b) // true (typeof === 'function')
671
+ * Callback.check(42) // false
672
+ *
673
+ * @example
674
+ * function add(a: 0, b: 0) -> 0 { return a + b }
675
+ * const Adder = FunctionPredicate('Adder', add)
676
+ * // Extracts params/returns from add.__tjs
677
+ */
678
+ export function FunctionPredicate(
679
+ name: string,
680
+ // eslint-disable-next-line @typescript-eslint/ban-types
681
+ specOrFn: FunctionPredicateSpec | Function
682
+ ): FunctionPredicateType {
683
+ let params: Record<string, any> = {}
684
+ let returns: any = undefined
685
+ let returnContract: ReturnContract = 'assertReturns'
686
+
687
+ if (typeof specOrFn === 'function') {
688
+ // Extract from function's __tjs metadata
689
+ const meta = (specOrFn as any).__tjs
690
+ if (meta) {
691
+ // Build params from __tjs.params
692
+ if (meta.params) {
693
+ for (const [key, info] of Object.entries(meta.params)) {
694
+ params[key] = (info as any)?.example ?? null
695
+ }
696
+ }
697
+ // Extract return type
698
+ if (meta.returns) {
699
+ returns = (meta.returns as any)?.example ?? null
700
+ }
701
+ // Extract return contract from safety markers
702
+ if (meta.safeReturn) returnContract = 'checkedReturns'
703
+ else if (meta.unsafe) returnContract = 'assertReturns'
704
+ else returnContract = 'returns'
705
+ }
706
+ } else {
707
+ params = specOrFn.params ?? {}
708
+ returns = specOrFn.returns
709
+ returnContract = specOrFn.returnContract ?? 'assertReturns'
710
+ }
711
+
712
+ const fpType: FunctionPredicateType = {
713
+ description: name,
714
+ params,
715
+ returns,
716
+ returnContract,
717
+ // eslint-disable-next-line @typescript-eslint/ban-types
718
+ check: (value: unknown): value is Function => {
719
+ if (typeof value !== 'function') return false
720
+
721
+ // Structural validation: check arity and __tjs metadata
722
+ const expectedArity = Object.keys(params).length
723
+ if (expectedArity > 0) {
724
+ // Check function.length (number of params before first default)
725
+ // eslint-disable-next-line @typescript-eslint/ban-types
726
+ const fn = value as Function
727
+ const meta = (fn as any).__tjs
728
+ if (meta?.params) {
729
+ // Has TJS metadata — check param count matches
730
+ const metaParamCount = Object.keys(meta.params).length
731
+ if (metaParamCount !== expectedArity) return false
732
+
733
+ // Check param type kinds match where both sides have type info
734
+ const expectedKeys = Object.keys(params)
735
+ const metaKeys = Object.keys(meta.params)
736
+ for (let i = 0; i < expectedKeys.length; i++) {
737
+ const metaInfo = meta.params[metaKeys[i]]
738
+ const expectedExample = params[expectedKeys[i]]
739
+ if (metaInfo?.type?.kind && expectedExample !== undefined) {
740
+ const expectedKind = kindOfExample(expectedExample)
741
+ if (
742
+ expectedKind &&
743
+ metaInfo.type.kind !== expectedKind &&
744
+ metaInfo.type.kind !== 'any'
745
+ )
746
+ return false
747
+ }
748
+ }
749
+ }
750
+ }
751
+
752
+ return true
753
+ },
754
+ __runtimeType: true as const,
755
+ }
756
+
757
+ return fpType
758
+ }
@@ -38,6 +38,11 @@ export {
38
38
  TRecord,
39
39
  type GenericType,
40
40
  type TypeParam,
41
+ // Function predicates
42
+ FunctionPredicate,
43
+ type FunctionPredicateType,
44
+ type FunctionPredicateSpec,
45
+ type ReturnContract,
41
46
  } from './Type'
42
47
 
43
48
  // Timestamp and LegalDate utilities (pure functions)