Skip to content

A default in type_params reaches a type-argument position, where it is not valid #112

Description

@madonoharu

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions