Skip to content

Bug: --actor-interface-file output is not equivalent to the .ts file (no class declaration, different createActor return type) #158

Description

@marc0olo

--actor-interface-file is documented as generating "a <service-name>.d.ts file that contains the same types of the <service-name>.ts file" (icp-bindgen.ts#L42). It does not: the .d.ts omits the actor class entirely and gives createActor a different return type. Both files are individually valid and typecheck, so this is a fidelity gap rather than a breakage — but the two are not interchangeable, and the docs describe a third thing again.

Evidence (committed snapshots)

tests/snapshots/generate/hello_world/hello_world.ts.snapshot — the class exists and createActor returns it:

export class Hello_world implements hello_worldInterface {
    constructor(private actor: ActorSubclass<_SERVICE>){}
export function createActor(canisterId: string, options: CreateActorOptions = {}): Hello_world {

tests/snapshots/generate/hello_world/hello_world.d.ts.snapshotno class declaration at all, and createActor returns the interface:

export interface Some<T> {  }
export interface None {  }
export type Option<T> = Some<T> | None;
export interface hello_worldInterface {  }
export interface CreateActorOptions {  }
export declare function createActor(canisterId: string, options: CreateActorOptions = {}): hello_worldInterface;

And docs/src/content/docs/structure.md documents the interface:

function createActor(canisterId: string, options: CreateActorOptions = {}): <service-name>Interface;

So Hello_world is not a name the .d.ts consumer can refer to at all, and code that compiles against the .ts can fail against the .d.ts.

The obvious fix is not available

Making the wrapper return hello_worldInterface would align all three in a one-line change. It is a breaking change and should not be done.

The class takes constructor(private actor: …), and a private member makes the class type effectively nominal — the interface is not assignable to it:

declare const asInterface: hello_worldInterface;
const x: Hello_world = asInterface;
// error TS2741: Property 'actor' is missing in type 'hello_worldInterface'
//              but required in type 'Hello_world'.

Assignability only runs one way: class → interface works, interface → class does not. So returning the class is the strictly more permissive signature — both const a: Hello_world = createActor(id) and const a: hello_worldInterface = createActor(id) compile today. Narrowing the wrapper to the interface would break the first, in the primary artifact, to fix a mismatch in an opt-in one.

Suggested fix

Bring the .d.ts up to the .ts, not the reverse:

  1. Emit export declare class <ServiceName> implements <service-name>Interface { … } in the interface file — constructor plus method signatures. compile_interface.rs already builds the method signatures for the interface, so this is a declaration-only variant of create_actor_class.
  2. Include private actor; in that declaration. Without it the .d.ts class is structurally typed while the .ts class is nominal, so the two artifacts would still differ — just more subtly, which is worse than the current honest gap.
  3. Change createActor in the .d.ts to return the class, and update structure.md to document the class rather than the interface.

Cheaper alternative if the codegen work is not worth it now: document both signatures accurately and drop the "same types" claim from the --actor-interface-file help text. That removes the false statement without touching generation, but leaves the two artifacts non-interchangeable.

Notes

Relabelled from documentation to bug: the docs are wrong, but the underlying defect is in what the flag generates.

Ruled out while investigating — the options: CreateActorOptions = {} initializer in the declaration file is valid TypeScript (a parameter initializer in an ambient declaration simply marks the parameter optional; tsc accepts it).

Verified on @icp-sdk/bindgen@0.4.0 and main, with TypeScript 5.9.3.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrustPull requests that update rust code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions