Migrating from 0.3.0

What changes in @candid-core/schema 0.4.0 and @candid-core/cli 0.3.0, published on 2026-10-10, for code written against @candid-core/schema 0.3.0 and @candid-core/cli 0.2.0: method modes and plain type names in generated modules, which need the two packages upgraded together.

This page is for code written against @candid-core/schema 0.3.0 and @candid-core/cli 0.2.0, the stable pair published on 2026-10-06, and takes it to the next one, @candid-core/schema 0.4.0 and @candid-core/cli 0.3.0, published on 2026-10-10. The npm latest dist-tag has moved on since, to 0.5.0 and 0.4.0, published on 2026-10-27, and Migrating from 0.4.0 covers that step. Every change is recorded under those two versions in the @candid-core/schema changelog and the @candid-core/cli changelog. Coming from 0.2.0 and 0.1.0, read Migrating from 0.2.0 first.

The upgrade is small. @candid-core/schema 0.4.0 breaks nothing in 0.3.0's API: it adds two type-only exports, WithMode and ModeOf, and its runtime is 0.3.0's. What changes is the text of the modules the CLI generates, and that text needs the new schema release.

What changedWhat it breaksWhere
Each method of a generated Actor carries its mode, $.WithMode<mode>A module generated by 0.3.0 compiled against @candid-core/schema 0.3.0; a test that compares a method type for exact equalityMethod modes
Each generated type is declared under its Candid nameCommitted modules, which gen --check now reports as drifted; code that parses or patches generated sourceType names
The CLI's peer is exactly @candid-core/schema 0.4.0Upgrading one package without the otherOrder of upgrade
project and check, projectDid and checkCompatibleNothing: they are newProjection and compatibility
The snippets below are compiled

Each block is checked against this tree, with ./ledger.ts standing for a module CLI 0.3.0 generates. The before block must fail with the error shown, and the after block must compile.

Method modes#

A generated Actor types each method as its call signature intersected with $.WithMode<"query">, "composite_query", "update" or "oneway", and ModeOf from @candid-core/schema reads the mode back, so a call layer can refuse an update where it builds a read at compile time. The mark is one optional property under a symbol no value has: the call signature, Parameters<>, ReturnType<>, keyof Actor and the module's exports are unchanged, and a plain async function still implements a method.

tscrates/candid-core-ts/tests/goldens/ledger.ts
export type Actor = {
  fee: (() => Promise<Tokens>) & $.WithMode<"query">;
  // …
  transfer: ((arg0: TransferArg) => Promise<TransferResult>) & $.WithMode<"update">;
  // …
};

WithMode is new in @candid-core/schema 0.4.0, so a module with an actor method does not compile against 0.3.0: tsc reports TS2694, "Namespace … has no exported member 'WithMode'", once per method. That is why the CLI peers 0.4.0 exactly. A module the published 0.2.0 generated compiles against 0.4.0 unchanged.

The one kind of consumer code that sees the mark is a type-level test that compares a method type for exact equality with its bare signature:

ts
import type { Actor, Tokens } from "./ledger.ts";

type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;

// A 0.3.0-era test: the method type is exactly its call signature.
const exact: Equal<Actor["fee"], () => Promise<Tokens>> = true;
void exact;

Compare the parts a call layer reads instead, and the mode, which is new:

ts
import type { ModeOf } from "@candid-core/schema";
import type { Actor, Tokens } from "./ledger.ts";

type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;

const args: Equal<Parameters<Actor["fee"]>, []> = true;
const result: Equal<ReturnType<Actor["fee"]>, Promise<Tokens>> = true;
const read: ModeOf<Actor["fee"]> = "query";
const write: ModeOf<Actor["transfer"]> = "update";
const fee: Actor["fee"] = async () => ({ e8s: 10_000n });
void [args, result, read, write, fee];

Type names#

A generated module declares each declaration's type under its Candid name and keeps the $-prefixed local for the schema value only, so a compiler error names Tokens and Actor, the names you import, where 0.2.0's named $Tokens and $Actor. Every export name, and what each one is as a type and as a value, is unchanged, and so is each module's set of declarations. A name that cannot be a type in the module keeps the $ local for its type too: the global types the generated types use (Array, Promise, Record, Uint8Array) and the words TypeScript refuses as a type's name (delete, string, default, keyof, …).

tscrates/candid-core-ts/tests/goldens/ledger.ts
export type Tokens = { e8s: bigint };
const $Tokens: $.Schema<Tokens> = $.c.rec(() => $.c.record({ e8s: $.c.nat64 }));
export { $Tokens as Tokens };

0.2.0 wrote the same declaration as type $Tokens = { e8s: bigint }; and const $Tokens: $.Schema<$Tokens> = …. Code that imports the generated names is unaffected. Committed modules are not: candid-core-cli gen --check compares bytes, so every module 0.2.0 wrote for an interface with a declaration or an actor is reported as drifted, exit 1, until gen runs again. The envelope beside it is byte-identical, and --check reports it unchanged: no Contract and no identity moves.

Order of upgrade#

Move both packages in one step. The CLI peers its schema release exactly, and npm refuses a mismatched pair (ERESOLVE could not resolve): @candid-core/cli 0.2.0 peers 0.3.0, and 0.3.0 peers 0.4.0. The plain lines select the newest stable pair from latest, 0.5.0 and 0.4.0, which takes the next step as well (Migrating from 0.4.0):

bash
npm install --save-exact @candid-core/schema
npm install --save-dev --save-exact @candid-core/cli

To stop at this page's pair, name its versions:

bash
npm install --save-exact @candid-core/[email protected]
npm install --save-dev --save-exact @candid-core/[email protected]

Then run candid-core-cli gen over every committed module, and fix what the compiler reports, which for code that imports the generated names is nothing but an exact-equality test like the one above.