Corrected 2026-08-30. The first version of this issue said the failure came
from splitting the argument on every comma, and that a single parameter with no
comma in it worked. Measuring it while writing the e2e case in #126 showed
otherwise: the comma is not what breaks, and the case I offered as working does
not. The comma split is real, but it is a second finding rather than the cause.
A namespaced enum writes a union that references the type declared inside the
namespace, and it passes the whole #[tsify(type_params = "...")] spec through
as the type argument. A default is legal where the parameter is declared and
not where it is used, so any spec carrying an = produces TypeScript that does
not parse.
Reproducing
On 7bf7750, checked with TypeScript 5.9.3:
#[derive(Tsify)] #[tsify(namespace, type_params = "T = Record<string, number>")]
enum CompoundDefault<T> { V(T) }
#[derive(Tsify)] #[tsify(namespace, type_params = "T = string")]
enum SimpleDefault<T> { V(T) }
#[derive(Tsify)] #[tsify(namespace, type_params = "T")]
enum NoDefault<T> { V(T) }
declare namespace CompoundDefault {
export type V<T = Record<string, number>> = { V: T };
}
export type CompoundDefault<T> = CompoundDefault.V<T = Record<string, number>>;
declare namespace SimpleDefault {
export type V<T = string> = { V: T };
}
export type SimpleDefault<T> = SimpleDefault.V<T = string>;
declare namespace NoDefault {
export type V<T> = { V: T };
}
export type NoDefault<T> = NoDefault.V<T>;
error TS1005: '>' expected. // CompoundDefault union
error TS1109: Expression expected.
error TS1005: '>' expected. // SimpleDefault union
error TS1109: Expression expected.
Every declaration inside a namespace is correct. Both unions carrying a default
fail, and they fail identically whether or not the default contains a comma.
NoDefault is fine.
Outside a namespace there is no union, so the default lands in a declaration
where it is legal and the output type-checks:
#[derive(Tsify)] #[tsify(type_params = "T = Record<string, number>")]
struct CompoundDefaultStruct<T> { data: T }
export interface CompoundDefaultStruct<T = Record<string, number>> { data: T; }
Where it goes wrong
The union writes the parameter spec it was given rather than the parameter
names it declared. A reference should use the names alone.
The comma split, which is a separate matter
attrs.rs parses the attribute with lit.value().split(','), which does not
know about the brackets a type can carry, so Record<string, number> is taken
as two parameters — T = Record<string and number>. That much is in the
source.
What I could not do is show it in the output. The declaration rejoins the
fragments when it prints them, and the union prints the same string either way,
so nothing in the generated .d.ts distinguishes a split spec from a rejoined
one. If the split has an observable consequence, I have not found the case that
shows it — a fix should come with one.
Splitting correctly is not just a bracket-depth counter: a comma inside a string
literal type (T = "a,b") or a template literal type sits at depth zero and is
still not a separator, so this wants a small tokenizer that tracks quotes and
escapes.
Notes
Neither part is new. They predate #110, which is where I ran into this — that
PR gives defaults a way to reach TypeScript, so a compound default became
something you could write and see fail. The attribute has accepted the same
string all along.
The current output for all of these is recorded in
tests-e2e/reference_output/test_type_params1/ as of #126, so a fix shows up as
a diff there.
Corrected 2026-08-30. The first version of this issue said the failure came
from splitting the argument on every comma, and that a single parameter with no
comma in it worked. Measuring it while writing the e2e case in #126 showed
otherwise: the comma is not what breaks, and the case I offered as working does
not. The comma split is real, but it is a second finding rather than the cause.
A namespaced enum writes a union that references the type declared inside the
namespace, and it passes the whole
#[tsify(type_params = "...")]spec throughas the type argument. A default is legal where the parameter is declared and
not where it is used, so any spec carrying an
=produces TypeScript that doesnot parse.
Reproducing
On
7bf7750, checked with TypeScript 5.9.3:Every declaration inside a namespace is correct. Both unions carrying a default
fail, and they fail identically whether or not the default contains a comma.
NoDefaultis fine.Outside a namespace there is no union, so the default lands in a declaration
where it is legal and the output type-checks:
Where it goes wrong
The union writes the parameter spec it was given rather than the parameter
names it declared. A reference should use the names alone.
The comma split, which is a separate matter
attrs.rsparses the attribute withlit.value().split(','), which does notknow about the brackets a type can carry, so
Record<string, number>is takenas two parameters —
T = Record<stringandnumber>. That much is in thesource.
What I could not do is show it in the output. The declaration rejoins the
fragments when it prints them, and the union prints the same string either way,
so nothing in the generated
.d.tsdistinguishes a split spec from a rejoinedone. If the split has an observable consequence, I have not found the case that
shows it — a fix should come with one.
Splitting correctly is not just a bracket-depth counter: a comma inside a string
literal type (
T = "a,b") or a template literal type sits at depth zero and isstill not a separator, so this wants a small tokenizer that tracks quotes and
escapes.
Notes
Neither part is new. They predate #110, which is where I ran into this — that
PR gives defaults a way to reach TypeScript, so a compound default became
something you could write and see fail. The attribute has accepted the same
string all along.
The current output for all of these is recorded in
tests-e2e/reference_output/test_type_params1/as of #126, so a fix shows up asa diff there.