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 changed | What it breaks | Where |
|---|---|---|
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 equality | Method modes |
| Each generated type is declared under its Candid name | Committed modules, which gen --check now reports as drifted; code that parses or patches generated source | Type names |
The CLI's peer is exactly @candid-core/schema 0.4.0 | Upgrading one package without the other | Order of upgrade |
project and check, projectDid and checkCompatible | Nothing: they are new | Projection and compatibility |
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.
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:
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:
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, …).
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):
npm install --save-exact @candid-core/schema
npm install --save-dev --save-exact @candid-core/cliTo stop at this page's pair, name its versions:
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.