Skip to content

Add pitfall: bindgen wrapper converts opt T to T | null #141

@marc0olo

Description

@marc0olo

Problem

AI agents (and developers migrating from dfx) have strong prior knowledge that Candid opt T is represented as [] | [T] in TypeScript. This was the standard pattern in @dfinity/agent and dfx-generated declarations for years.

@icp-sdk/bindgen generates a wrapper class (e.g. Backend) on top of the raw declarations that converts opt T to idiomatic T | null. Since createActor returns the wrapper, not the raw actor, code that uses the old array-style pattern breaks at runtime:

// Runtime error: Cannot read properties of null (reading 'length')
const result = await backend.getNickname();
if (result.length > 0) { name = result[0]; }

The runtime error gives no hint about the actual cause, making it hard to diagnose.

Why this would help agents

Agents read the binding-generation skill before writing frontend code that calls canisters. When they see opt text in a .did file, they default to prior training data which overwhelmingly uses the [] | [T] pattern. Without an explicit callout, agents consistently produce the wrong check — this happened in practice when building a simple hello-world app with getNickname : () -> (opt text) query.

With this pitfall documented, the agent sees the correct pattern at the moment it's writing the relevant code.

Proposed addition to references/binding-generation.md

opt T is T | null in the wrapper, not [] | [T]

@icp-sdk/bindgen generates two layers: raw declarations under declarations/ use the standard @icp-sdk/core/candid representation where opt T is [] | [T], and a wrapper class (e.g. Backend) that converts this to idiomatic T | null. Since createActor returns the wrapper class, always use T | null:

// Wrong — raw Candid style (only applies if using declarations directly)
const result = await backend.getNickname();
if (result.length > 0) { name = result[0]; }

// Correct — wrapper returns T | null
const result = await backend.getNickname();
if (result !== null) { name = result; }

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions