tjs-lang 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
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",
@@ -28,11 +28,11 @@ describe('TS → TJS conversion quality', () => {
28
28
  expect(code).not.toContain('x: number')
29
29
  })
30
30
 
31
- it('converts optional param to equals syntax', () => {
31
+ it('converts optional param to union with undefined', () => {
32
32
  const ts = `function greet(name?: string): string { return name || 'World' }`
33
33
  const { code } = fromTS(ts, { emitTJS: true })
34
34
 
35
- expect(code).toContain("name = ''")
35
+ expect(code).toContain("name: '' | undefined")
36
36
  expect(code).not.toContain('name?')
37
37
  })
38
38
 
@@ -50,12 +50,11 @@ describe('TS → TJS conversion quality', () => {
50
50
  expect(code).toContain('flag: false')
51
51
  })
52
52
 
53
- it('converts optional boolean param to = false', () => {
53
+ it('converts optional boolean param to union with undefined', () => {
54
54
  const ts = `function greet(name: string, excited?: boolean): string { return excited ? name + '!' : name }`
55
55
  const { code } = fromTS(ts, { emitTJS: true })
56
56
 
57
- expect(code).toContain('excited = false')
58
- expect(code).not.toContain('excited = true')
57
+ expect(code).toContain('excited: false | undefined')
59
58
  })
60
59
 
61
60
  it('converts array param correctly', () => {
@@ -86,7 +85,7 @@ describe('TS → TJS conversion quality', () => {
86
85
  const { code } = fromTS(ts, { emitTJS: true })
87
86
 
88
87
  expect(code).toContain("url: ''")
89
- expect(code).toContain('timeout = 0')
88
+ expect(code).toContain('timeout: 0.0 | undefined')
90
89
  })
91
90
  })
92
91
 
@@ -296,18 +295,18 @@ class Api {
296
295
  })
297
296
 
298
297
  describe('nullable types', () => {
299
- it('converts T | null to T || null', () => {
298
+ it('converts T | null to T | null', () => {
300
299
  const ts = `function maybe(x: string | null): string | null { return x }`
301
300
  const { code } = fromTS(ts, { emitTJS: true })
302
301
 
303
- expect(code).toContain("'' || null")
302
+ expect(code).toContain("'' | null")
304
303
  })
305
304
 
306
- it('converts T | undefined to T || undefined', () => {
305
+ it('converts T | undefined to T | undefined', () => {
307
306
  const ts = `function maybe(x: number | undefined): number | undefined { return x }`
308
307
  const { code } = fromTS(ts, { emitTJS: true })
309
308
 
310
- expect(code).toContain('0 || undefined')
309
+ expect(code).toContain('0 | undefined')
311
310
  })
312
311
  })
313
312
 
@@ -379,12 +378,21 @@ class Api {
379
378
 
380
379
  describe('TJS → JS transpilation quality', () => {
381
380
  describe('colon syntax transformation', () => {
382
- it('transforms colon params to defaults in output', () => {
381
+ it('strips colon type annotations from output (required params get no default)', () => {
383
382
  const source = `function greet(name: 'World') { return name }`
384
383
  const { code } = tjs(source)
385
384
 
386
- expect(code).toContain('name = ')
385
+ // Required params (`:` syntax) should have no default in JS
386
+ expect(code).toContain('function greet(name)')
387
387
  expect(code).not.toContain("name: 'World'")
388
+ expect(code).not.toContain('name = ')
389
+ })
390
+
391
+ it('preserves defaults for optional params (= syntax)', () => {
392
+ const source = `function greet(name = 'World') { return name }`
393
+ const { code } = tjs(source)
394
+
395
+ expect(code).toContain("name = 'World'")
388
396
  })
389
397
  })
390
398
 
@@ -632,12 +640,12 @@ console.log(second())
632
640
  expect(code).not.toContain('y: string')
633
641
  })
634
642
 
635
- it('uses equals syntax for optional params', () => {
643
+ it('uses union with undefined for optional params', () => {
636
644
  const ts = `function test(x?: number, y?: string): void { }`
637
645
  const { code } = fromTS(ts, { emitTJS: true })
638
646
 
639
- expect(code).toContain('x = 0')
640
- expect(code).toContain("y = ''")
647
+ expect(code).toContain('x: 0.0 | undefined')
648
+ expect(code).toContain("y: '' | undefined")
641
649
  })
642
650
 
643
651
  it('uses -! syntax for return types (skip signature test)', () => {
@@ -1255,7 +1263,9 @@ function test(required: string, optional?: number): void { }
1255
1263
  const { types } = tjs(tjsCode)
1256
1264
 
1257
1265
  expect(types?.test?.params?.required?.required).toBe(true)
1258
- expect(types?.test?.params?.optional?.required).toBe(false)
1266
+ // TS optional becomes TJS union with undefined (required param that accepts undefined)
1267
+ expect(types?.test?.params?.optional?.required).toBe(true)
1268
+ expect(types?.test?.params?.optional?.type?.kind).toBe('union')
1259
1269
  })
1260
1270
  })
1261
1271
  })
@@ -1788,7 +1798,7 @@ function format(x: '') -! '' { return x + '!' }
1788
1798
  })
1789
1799
 
1790
1800
  it('error short-circuits function body', () => {
1791
- let bodyExecuted = false
1801
+ const bodyExecuted = false
1792
1802
 
1793
1803
  const { code } = tjs(`
1794
1804
  function process(x: '') -! '' {
@@ -1935,4 +1945,46 @@ function divide(a: 10, b: 2) -? { value: 0, error = '' } {
1935
1945
  expect(result.code).toContain('"error"')
1936
1946
  })
1937
1947
  })
1948
+
1949
+ describe('union param JS output', () => {
1950
+ it('required union param has no default in JS', () => {
1951
+ const result = tjs('function f(x: false | undefined) { return x }')
1952
+ expect(result.code).toContain('function f(x)')
1953
+ expect(result.code).not.toMatch(/x = false/)
1954
+ expect(result.code).not.toMatch(/x = false \| undefined/)
1955
+ })
1956
+
1957
+ it('optional union param keeps first value as default', () => {
1958
+ const result = tjs('function f(x = false | undefined) { return x }')
1959
+ expect(result.code).toContain('x = false')
1960
+ expect(result.code).not.toMatch(/x = false \| undefined/)
1961
+ })
1962
+
1963
+ it('generates union type check for required union param', () => {
1964
+ const result = tjs('function f(x: false | undefined) { return x }')
1965
+ expect(result.code).toContain("typeof x !== 'boolean'")
1966
+ expect(result.code).toContain('x !== undefined')
1967
+ })
1968
+
1969
+ it('generates nullable integer check for int|null', () => {
1970
+ const result = tjs('function f(x: 0 | null) { return x }')
1971
+ expect(result.code).toContain('function f(x)')
1972
+ // 0 | null is parsed as { kind: 'integer', nullable: true }, not a union
1973
+ expect(result.code).toContain("'integer'")
1974
+ // Nullable types skip the check when value is null
1975
+ expect(result.metadata?.f?.params?.x?.type?.nullable).toBe(true)
1976
+ })
1977
+
1978
+ it('preserves | in function body (bitwise OR)', () => {
1979
+ const result = tjs('function f(x: 0) { return x | 0xFF }')
1980
+ expect(result.code).toContain('x | 0xFF')
1981
+ })
1982
+
1983
+ it('preserves union metadata despite stripping from JS', () => {
1984
+ const result = tjs('function f(x: false | undefined) { return x }')
1985
+ const meta = result.metadata?.f
1986
+ expect(meta).toBeDefined()
1987
+ expect(meta.params.x.type.kind).toBe('union')
1988
+ })
1989
+ })
1938
1990
  })
@@ -139,11 +139,6 @@ function typeToExample(
139
139
  case ts.SyntaxKind.NumberKeyword:
140
140
  return '0.0'
141
141
  case ts.SyntaxKind.BooleanKeyword:
142
- // REVISIT: TS `x?: boolean` becomes TJS `x = false`, which collapses
143
- // "not passed" (undefined) and "passed as false" into the same value.
144
- // Code that distinguishes the three states (true/false/undefined) will
145
- // break. Consider emitting `x: false || null` for optional booleans
146
- // to preserve the undefined state.
147
142
  return 'false'
148
143
  case ts.SyntaxKind.NullKeyword:
149
144
  return 'null'
@@ -301,10 +296,10 @@ function typeToExample(
301
296
  const hasUndefined = unionType.types.some(isUndefinedType)
302
297
 
303
298
  if (nonNullTypes.length === 1 && (hasNull || hasUndefined)) {
304
- // Nullable type: T | null -> T || null
299
+ // Nullable type: T | null -> T | null
305
300
  const baseExample = typeToExample(nonNullTypes[0], checker)
306
- if (hasNull) return `${baseExample} || null`
307
- if (hasUndefined) return `${baseExample} || undefined`
301
+ if (hasNull) return `${baseExample} | null`
302
+ if (hasUndefined) return `${baseExample} | undefined`
308
303
  }
309
304
 
310
305
  // General union: use first type as example
@@ -921,7 +916,7 @@ function transformFunctionToTJS(
921
916
  warnings?: string[],
922
917
  includeLineNumber?: boolean
923
918
  ): string {
924
- const params: string[] = []
919
+ const params = transformParams(node.parameters, sourceFile, warnings)
925
920
 
926
921
  // Get line number (1-indexed) for source mapping
927
922
  const { line } = sourceFile.getLineAndCharacterOfPosition(
@@ -929,27 +924,6 @@ function transformFunctionToTJS(
929
924
  )
930
925
  const lineComment = includeLineNumber ? `/* line ${line + 1} */\n` : ''
931
926
 
932
- for (const param of node.parameters) {
933
- const name = param.name.getText(sourceFile)
934
- const isOptional = !!param.questionToken || !!param.initializer
935
- const typeExample = typeToExample(param.type, undefined, warnings)
936
-
937
- if (param.initializer) {
938
- // Has default value - use it directly
939
- const defaultText = param.initializer.getText(sourceFile)
940
- params.push(`${name} = ${defaultText}`)
941
- } else if (typeExample === 'any' || typeExample === 'undefined') {
942
- // any/undefined type - no annotation in TJS (bare name means any)
943
- params.push(name)
944
- } else if (isOptional) {
945
- // Optional without default - use = for optional
946
- params.push(`${name} = ${typeExample}`)
947
- } else {
948
- // Required - use : for required
949
- params.push(`${name}: ${typeExample}`)
950
- }
951
- }
952
-
953
927
  const funcName =
954
928
  explicitName ||
955
929
  (ts.isFunctionDeclaration(node) && node.name
@@ -1208,8 +1182,9 @@ function transformParams(
1208
1182
  // any/undefined type - no annotation in TJS (bare name means any)
1209
1183
  params.push(name)
1210
1184
  } else if (isOptional) {
1211
- // Optional without default - use = for optional
1212
- params.push(`${name} = ${typeExample}`)
1185
+ // Optional without default - use union with undefined to preserve
1186
+ // three-state semantics (e.g. TS `flag?: boolean` can be true/false/undefined)
1187
+ params.push(`${name}: ${typeExample} | undefined`)
1213
1188
  } else {
1214
1189
  // Required - use : for required
1215
1190
  params.push(`${name}: ${typeExample}`)
@@ -139,6 +139,26 @@ export interface TJSTypeInfo {
139
139
  destructuredRequired?: Set<string>
140
140
  }
141
141
 
142
+ /**
143
+ * Check if a param used `:` (required) or `=` (optional) in the raw source.
144
+ * Finds the function's param list by name, then looks for `paramName:` vs `paramName =`.
145
+ */
146
+ function isParamRequiredInSource(
147
+ source: string,
148
+ funcName: string,
149
+ paramName: string
150
+ ): boolean {
151
+ if (!source || !funcName) return false
152
+ // Find the function declaration and its param list
153
+ const funcPattern = new RegExp(
154
+ `function\\s+${funcName}\\s*\\([^)]*?\\b${paramName}\\s*([=:])`,
155
+ 's'
156
+ )
157
+ const match = source.match(funcPattern)
158
+ if (!match) return false
159
+ return match[1] === ':'
160
+ }
161
+
142
162
  /**
143
163
  * Extract type info for a single function declaration
144
164
  */
@@ -146,7 +166,8 @@ function extractFunctionTypeInfo(
146
166
  func: FunctionDeclaration,
147
167
  originalSource: string,
148
168
  requiredParams: Set<string>,
149
- returnTypeStr: string | null
169
+ returnTypeStr: string | null,
170
+ inputSource?: string
150
171
  ): { types: TJSTypeInfo; warnings: string[] } {
151
172
  const warnings: string[] = []
152
173
 
@@ -205,9 +226,19 @@ function extractFunctionTypeInfo(
205
226
  param.left.type === 'Identifier'
206
227
  ) {
207
228
  const paramInfo = parseParameter(param, requiredParams)
229
+ // Determine if this param used `:` (required) or `=` (optional).
230
+ // The global requiredParams set is name-based, which fails when
231
+ // two functions share a param name with different syntax.
232
+ // Use the raw input source to check the actual syntax.
233
+ const isRequired = isParamRequiredInSource(
234
+ inputSource || '',
235
+ func.id?.name || '',
236
+ param.left.name
237
+ )
208
238
  params[param.left.name] = {
209
239
  ...paramInfo,
210
- required: requiredParams.has(param.left.name),
240
+ required: isRequired,
241
+ default: isRequired ? null : paramInfo.example ?? paramInfo.default,
211
242
  description: tdoc.params[param.left.name],
212
243
  }
213
244
  } else if (param.type === 'ObjectPattern') {
@@ -346,7 +377,10 @@ function generateInlineValidationCode(
346
377
  const typeCheck = generateTypeCheckExpr(paramName, param.type)
347
378
 
348
379
  if (typeCheck) {
349
- const expectedType = param.type.kind
380
+ const expectedType =
381
+ param.type.kind === 'union'
382
+ ? (param.type as any).members.map((m: any) => m.kind).join(' | ')
383
+ : param.type.kind
350
384
  if (param.required) {
351
385
  lines.push(
352
386
  `if (${typeCheck}) return __tjs.typeError('${path}', '${expectedType}', ${paramName});`
@@ -533,6 +567,9 @@ export function transpileToJS(
533
567
 
534
568
  // Collect insertions: { position, text } to be applied in reverse order
535
569
  const insertions: { position: number; text: string }[] = []
570
+ // Collect deletions for | union suffixes in param defaults
571
+ // e.g. `x = false | undefined` -> `x = false` (the `| undefined` is type-only)
572
+ const deletions: { start: number; end: number }[] = []
536
573
 
537
574
  // Process each function
538
575
  for (const func of functions) {
@@ -564,11 +601,40 @@ export function transpileToJS(
564
601
  func,
565
602
  originalSource,
566
603
  requiredParams,
567
- returnTypeStr
604
+ returnTypeStr,
605
+ cleanSource
568
606
  )
569
607
  warnings.push(...funcWarnings)
570
608
  allTypes[funcName] = types
571
609
 
610
+ // Clean up param defaults in the emitted JS.
611
+ // After colon→equals transform, `x: false | undefined` becomes
612
+ // `x = false | undefined` in the parsed source.
613
+ // - For required params (`:` syntax), strip the entire `= value` — there's
614
+ // no JS default for required params, the value is a type annotation only.
615
+ // - For union defaults, strip just the `| suffix` to avoid bitwise OR.
616
+ for (const param of func.params) {
617
+ if (param.type === 'AssignmentPattern') {
618
+ const paramName =
619
+ (param as any).left?.name || (param as any).left?.value
620
+ const paramInfo = paramName ? types.params[paramName] : null
621
+
622
+ if (paramInfo?.required && paramInfo.default === null) {
623
+ // Required param — strip entire `= value` from JS output
624
+ deletions.push({
625
+ start: (param as any).left.end,
626
+ end: (param as any).right.end,
627
+ })
628
+ } else {
629
+ // Optional param with union — strip just the `| suffix`
630
+ const right = (param as any).right
631
+ if (right.type === 'BinaryExpression' && right.operator === '|') {
632
+ deletions.push({ start: right.left.end, end: right.end })
633
+ }
634
+ }
635
+ }
636
+ }
637
+
572
638
  // Determine safety options
573
639
  // Module-level "safety none" makes ALL functions unsafe (no validation)
574
640
  const isUnsafe =
@@ -649,10 +715,27 @@ export function transpileToJS(
649
715
  }
650
716
  }
651
717
 
652
- // Apply insertions in reverse position order (to maintain correct offsets)
653
- insertions.sort((a, b) => b.position - a.position)
654
-
718
+ // Apply deletions first (reverse order to maintain offsets), then insertions.
719
+ // Deletions strip | union suffixes from param defaults in the output JS.
720
+ deletions.sort((a, b) => b.start - a.start)
655
721
  let code = preprocessed.source
722
+ for (const { start, end } of deletions) {
723
+ code = code.slice(0, start) + code.slice(end)
724
+ }
725
+
726
+ // Adjust insertion positions for any deletions that shifted offsets
727
+ for (const ins of insertions) {
728
+ let shift = 0
729
+ for (const del of deletions) {
730
+ if (del.start < ins.position) {
731
+ shift += del.end - del.start
732
+ }
733
+ }
734
+ ins.position -= shift
735
+ }
736
+
737
+ // Apply insertions in reverse position order
738
+ insertions.sort((a, b) => b.position - a.position)
656
739
  for (const { position, text } of insertions) {
657
740
  code = code.slice(0, position) + text + code.slice(position)
658
741
  }
@@ -1002,6 +1085,13 @@ function generateTypeCheckExpr(
1002
1085
  case 'object':
1003
1086
  // For nested objects, just check it's an object (deep validation is separate)
1004
1087
  return `(typeof ${fieldPath} !== 'object' || ${fieldPath} === null || Array.isArray(${fieldPath}))`
1088
+ case 'union': {
1089
+ const checks = (type as any).members
1090
+ .map((m: TypeDescriptor) => generateTypeCheckExpr(fieldPath, m))
1091
+ .filter((c: string | null) => c !== null)
1092
+ if (checks.length === 0) return null
1093
+ return `(${checks.join(' && ')})`
1094
+ }
1005
1095
  case 'any':
1006
1096
  return null // No check needed
1007
1097
  default:
@@ -19,13 +19,13 @@ describe('TypeScript to TJS Transpiler', () => {
19
19
  expect(result.code).toContain('b: 0')
20
20
  })
21
21
 
22
- it('should convert optional params to = syntax', () => {
22
+ it('should convert optional params to union with undefined', () => {
23
23
  const result = fromTS(
24
24
  `function greet(name: string, title?: string) { return name }`,
25
25
  { emitTJS: true }
26
26
  )
27
27
  expect(result.code).toContain("name: ''")
28
- expect(result.code).toContain("title = ''")
28
+ expect(result.code).toContain("title: '' | undefined")
29
29
  })
30
30
 
31
31
  it('should convert return type to -! annotation (skip signature test)', () => {
@@ -57,7 +57,7 @@ describe('TypeScript to TJS Transpiler', () => {
57
57
  `function find(id: string): string | null { return null }`,
58
58
  { emitTJS: true }
59
59
  )
60
- expect(result.code).toContain("-! '' || null") // -! for TS-transpiled
60
+ expect(result.code).toContain("-! '' | null") // -! for TS-transpiled
61
61
  })
62
62
 
63
63
  it('should preserve default values', () => {
@@ -6,8 +6,8 @@
6
6
  * 10 -> { kind: 'number' }
7
7
  * ['string'] -> { kind: 'array', items: { kind: 'string' } }
8
8
  * { name: 'string' } -> { kind: 'object', shape: { name: { kind: 'string' } } }
9
- * 'string' || null -> { kind: 'string', nullable: true }
10
- * 'string' || 0 -> { kind: 'union', members: [{ kind: 'string' }, { kind: 'number' }] }
9
+ * 'string' | null -> { kind: 'string', nullable: true }
10
+ * 'string' | 0 -> { kind: 'union', members: [{ kind: 'string' }, { kind: 'number' }] }
11
11
  */
12
12
 
13
13
  import { parseExpressionAt } from 'acorn'
@@ -71,24 +71,8 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
71
71
  const { operator, left, right } = node as any
72
72
 
73
73
  if (operator === '||') {
74
- const leftType = inferTypeFromValue(left)
75
- const rightType = inferTypeFromValue(right)
76
-
77
- // type || null means nullable type
78
- if (rightType.kind === 'null') {
79
- return { ...leftType, nullable: true }
80
- }
81
-
82
- // null || type means nullable type (reverse)
83
- if (leftType.kind === 'null') {
84
- return { ...rightType, nullable: true }
85
- }
86
-
87
- // type1 || type2 means union
88
- return {
89
- kind: 'union',
90
- members: [leftType, rightType],
91
- }
74
+ // || is JavaScript logical OR — infer type from left operand
75
+ return inferTypeFromValue(left)
92
76
  }
93
77
 
94
78
  if (operator === '&&') {
@@ -106,6 +90,27 @@ export function inferTypeFromValue(node: Expression): TypeDescriptor {
106
90
  return { kind: 'any' }
107
91
  }
108
92
 
93
+ case 'BinaryExpression': {
94
+ const { operator, left, right } = node as any
95
+ // | means union type (e.g., 0 | null, '' | undefined)
96
+ if (operator === '|') {
97
+ const leftType = inferTypeFromValue(left)
98
+ const rightType = inferTypeFromValue(right)
99
+
100
+ if (rightType.kind === 'null') {
101
+ return { ...leftType, nullable: true }
102
+ }
103
+ if (leftType.kind === 'null') {
104
+ return { ...rightType, nullable: true }
105
+ }
106
+ return {
107
+ kind: 'union',
108
+ members: [leftType, rightType],
109
+ }
110
+ }
111
+ return { kind: 'any' }
112
+ }
113
+
109
114
  case 'Identifier': {
110
115
  // Handle undefined as a type
111
116
  if ((node as any).name === 'undefined') {
@@ -282,6 +287,15 @@ export function extractLiteralValue(node: Expression): any {
282
287
  }
283
288
  return undefined
284
289
 
290
+ case 'BinaryExpression': {
291
+ const { operator, left } = node as any
292
+ // | is union type — extract the left (primary) example value
293
+ if (operator === '|') {
294
+ return extractLiteralValue(left)
295
+ }
296
+ return undefined
297
+ }
298
+
285
299
  case 'LogicalExpression': {
286
300
  const { operator, left, right } = node as any
287
301
  if (operator === '&&') {
@@ -294,9 +294,9 @@ test 'always fails' { throw new Error('intentional') }
294
294
  expect(signature.parameters.limit.default).toBe(10)
295
295
  })
296
296
 
297
- it('should handle nullable types with colon syntax', () => {
297
+ it('should handle nullable types with | syntax', () => {
298
298
  const { signature } = transpile(`
299
- function test(filter: 'default' || null) {
299
+ function test(filter: 'default' | null) {
300
300
  return { filter }
301
301
  }
302
302
  `)
@@ -304,9 +304,9 @@ test 'always fails' { throw new Error('intentional') }
304
304
  expect(signature.parameters.filter.type.nullable).toBe(true)
305
305
  })
306
306
 
307
- it('should handle union types with colon syntax', () => {
307
+ it('should handle union types with | syntax', () => {
308
308
  const { signature } = transpile(`
309
- function test(id: 'abc123' || 42) {
309
+ function test(id: 'abc123' | 42) {
310
310
  return { id }
311
311
  }
312
312
  `)
@@ -37,14 +37,16 @@ describe('TJS Emitter', () => {
37
37
  expect(result.types.test.params.optional.required).toBe(false)
38
38
  })
39
39
 
40
- it('should convert colon syntax to default values in output', () => {
40
+ it('should strip colon type annotations from output (no default for required params)', () => {
41
41
  const result = transpileToJS(`
42
42
  function greet(name: 'world') {
43
43
  return name
44
44
  }
45
45
  `)
46
- expect(result.code).toContain("name = 'world'")
46
+ // Required `:` params have no default in JS output
47
+ expect(result.code).toContain('function greet(name)')
47
48
  expect(result.code).not.toContain("name: 'world'")
49
+ expect(result.code).not.toContain("name = 'world'")
48
50
  })
49
51
 
50
52
  it('should handle object type annotations', () => {
@@ -225,7 +227,7 @@ function greet(name: 'world') {
225
227
  return x
226
228
  }
227
229
  `)
228
- expect(result.code).toContain('function test(x = 0)')
230
+ expect(result.code).toContain('function test(x)')
229
231
  expect(result.code).not.toContain('!')
230
232
  })
231
233
 
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { describe, test, expect } from 'bun:test'
16
- import { transpileToJS, fromTS } from './index'
16
+ import { transpileToJS, fromTS, tjs } from './index'
17
17
 
18
18
  // Helper to get the first function's metadata from the Record
19
19
  function getFirstFunc(metadata: Record<string, any>) {
@@ -125,9 +125,9 @@ describe('Basic Types', () => {
125
125
  // =============================================================================
126
126
 
127
127
  describe('Union Types', () => {
128
- test('union with || (TJS style)', () => {
128
+ test('union with | (string or integer)', () => {
129
129
  const { metadata } = transpileToJS(`
130
- function flexible(id: '' || 0) {
130
+ function flexible(id: '' | 0) {
131
131
  return String(id)
132
132
  }
133
133
  `)
@@ -135,9 +135,9 @@ describe('Union Types', () => {
135
135
  expect(getFirstFunc(metadata).params.id.type.members?.length).toBe(2)
136
136
  })
137
137
 
138
- test('nullable with || null', () => {
138
+ test('nullable with | null', () => {
139
139
  const { metadata } = transpileToJS(`
140
- function maybeString(s: '' || null) {
140
+ function maybeString(s: '' | null) {
141
141
  return s ?? 'default'
142
142
  }
143
143
  `)
@@ -153,18 +153,7 @@ describe('Union Types', () => {
153
153
  expect(types?.flexible.params.id.type.kind).toBe('union')
154
154
  })
155
155
 
156
- test('union return type with || (TJS style)', () => {
157
- const { metadata } = transpileToJS(`
158
- function find(id: 0) -! { name: '' } || null {
159
- return null
160
- }
161
- `)
162
- expect(getFirstFunc(metadata).returns?.kind).toBe('object')
163
- expect(getFirstFunc(metadata).returns?.nullable).toBe(true)
164
- })
165
-
166
- // TODO: Union return types like `{ obj } | null` not yet supported in parser
167
- test.skip('union return type with | (TS style)', () => {
156
+ test('union return type with | (nullable object)', () => {
168
157
  const { metadata } = transpileToJS(`
169
158
  function find(id: 0) -! { name: '' } | null {
170
159
  return null
@@ -858,6 +847,20 @@ describe('Real-World Patterns', () => {
858
847
  // Generic T becomes any, but structure is preserved
859
848
  expect(types?.chunk.params.array.type.kind).toBe('array')
860
849
  })
850
+
851
+ test('?: boolean transpiles to required union param with no JS default', () => {
852
+ const tjsCode = fromTS('function f(excited?: boolean) { return excited }', {
853
+ emitTJS: true,
854
+ }).code
855
+ // TJS should have union annotation
856
+ expect(tjsCode).toContain('excited: false | undefined')
857
+ const jsResult = tjs(tjsCode)
858
+ // JS should not have default or bitwise OR — `:` means required
859
+ expect(jsResult.code).not.toMatch(/excited = false/)
860
+ expect(jsResult.code).not.toMatch(/excited = false \| undefined/)
861
+ // Should have union type check
862
+ expect(jsResult.code).toContain("typeof excited !== 'boolean'")
863
+ })
861
864
  })
862
865
 
863
866
  // =============================================================================