--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.snapshot — no 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:
- 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.
- 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.
- 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.
--actor-interface-fileis documented as generating "a<service-name>.d.tsfile that contains the same types of the<service-name>.tsfile" (icp-bindgen.ts#L42). It does not: the.d.tsomits the actor class entirely and givescreateActora 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 andcreateActorreturns it:tests/snapshots/generate/hello_world/hello_world.d.ts.snapshot— no class declaration at all, andcreateActorreturns the interface:And
docs/src/content/docs/structure.mddocuments the interface:So
Hello_worldis not a name the.d.tsconsumer can refer to at all, and code that compiles against the.tscan fail against the.d.ts.The obvious fix is not available
Making the wrapper return
hello_worldInterfacewould 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: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)andconst 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.tsup to the.ts, not the reverse:export declare class <ServiceName> implements <service-name>Interface { … }in the interface file — constructor plus method signatures.compile_interface.rsalready builds the method signatures for the interface, so this is a declaration-only variant ofcreate_actor_class.private actor;in that declaration. Without it the.d.tsclass is structurally typed while the.tsclass is nominal, so the two artifacts would still differ — just more subtly, which is worse than the current honest gap.createActorin the.d.tsto return the class, and updatestructure.mdto 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-filehelp text. That removes the false statement without touching generation, but leaves the two artifacts non-interchangeable.Notes
Relabelled from
documentationtobug: 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;tscaccepts it).Verified on
@icp-sdk/bindgen@0.4.0andmain, with TypeScript 5.9.3.