Verified against bun-plugin-dtsx@0.21.17, emitting with Bun 1.4.0-canary.1+5b98630ac.
Symptom
An exported const whose initializer contains an arrow type inside a generic type argument emits syntactically invalid TypeScript. tsc rejects the generated .d.ts with TS1005.
Minimal reproduction
src/index.ts:
export function key<T>(name: string): symbol & { __t?: T } {
return Symbol(name) as symbol & { __t?: T }
}
/** A: generic argument with NO arrow type */
export const NoArrow = key<{ accent?: string }>('a')
/** B: generic argument CONTAINING an arrow type */
export const WithArrow = key<{ navigate: (path: string) => void }>('b')
/** C: two arrows in one generic argument */
export const TwoArrows = key<{ f: (x: string) => void, g: (y: number) => string }>('c')build.ts:
import { dts } from 'bun-plugin-dtsx'
await Bun.build({ entrypoints: ['./src/index.ts'], outdir: './dist', root: './src', plugins: [dts()] })Emitted dist/index.d.ts
export declare function key<T>(name: string): symbol & { __t?: T };
/** A: generic argument with NO arrow type */
export declare const NoArrow: unknown;
/** B: generic argument CONTAINING an arrow type */
export declare const WithArrow: (key<{ navigate: (path: string)) => unknown;
/** C: two arrows in one generic argument */
export declare const TwoArrows: (key<{ f: (x: string)) => any;$ tsc --noEmit --skipLibCheck false dist/index.d.ts
dist/index.d.ts(5,64): error TS1005: '=>' expected.
dist/index.d.ts(5,66): error TS1005: ';' expected.
dist/index.d.ts(7,54): error TS1005: '=>' expected.
dist/index.d.ts(7,56): error TS1005: ';' expected.What appears to be happening
NoArrow — no arrow anywhere in the initializer — widens to unknown correctly, which is the expected behaviour for a syntactic emitter that cannot resolve the type.
When an arrow is present, the emitter appears to treat the initializer as an arrow function: it opens a (, scans for the closing ), and takes the first one it finds rather than balancing. That first ) belongs to the inner arrow's parameter list, so the declaration is cut mid-type and the remainder of the generic argument (plus the real call arguments) is dropped, leaving unbalanced ( and {.
Note case C also shows the trailing type varying with the first inner arrow's return type — => unknown for void, => any for string — which is consistent with the inner arrow being mistaken for the outer one.
Where this ships today
@stacksjs/stx ships a broken declaration built by this plugin:
packages/stx/dist/composables.d.ts:227-234, from packages/stx/src/composables.ts:954-963:
export declare const RouterKey: (createInjectionKey<{
currentRoute: string
navigate: (path: string)) => unknown;
export declare const I18nKey: (createInjectionKey<{
locale: string
t: (key: string)) => unknown;Four TS1005 errors. This is present in the published @stacksjs/stx@0.2.152 from npm as well as in a local build, so it is not specific to a canary Bun or to one machine.
Scope in that package is narrow — 1 of 166 emitted .d.ts files — and it is currently latent rather than breaking consumers: the package's exports map points ./composables at the directory dist/composables/index.d.ts, nothing imports the bare ./composables specifier (which would resolve to the broken flat file, since TS prefers a file over a directory index), and most consumers run skipLibCheck: true. It would begin to bite on skipLibCheck: false, or if that resolution ever changes.
Suggested fix
Balance parentheses (and braces/angle brackets) when scanning an initializer for an arrow-function signature, rather than stopping at the first ). Failing that, falling back to unknown — as already happens for the no-arrow case — is strictly better than emitting output that does not parse.
Related
- stacksjs/stx#1796 — the same emitter falling back to
: voidfor inferred return types. Different failure mode: that one emits valid TypeScript with the wrong type; this one emits TypeScript that does not parse.