With typescript how can I directly use the types created by "dfx generate"?

There is probably an easier way but I generally update the generate script in package.json to chain it with a custom made script

UPDATE: indeed there is an easier way than following script:

// in dfx.json
"mycanister": {
  "declarations": {
    "node_compatibility": true,
     "env_override": ""
  }
}

source: With typescript how can I directly use the types created by "dfx generate"? - #3 by kpeacock


  "generate": "dfx generate && node scripts/update.types.mjs",

In this custom made script, I do two things:

  • I remove the default createActor and canisterId automatically generated because they crash my build and I want to use my own ids and actor functions
  • I move the auto generated .did.js to another file name such as .factory.did.js for TypeScript compatibility

The first part might not fits your need but the second my help you. e.g. such a script:

#!/usr/bin/env node

import { existsSync, readdirSync } from 'fs';
import { readFile, rename, writeFile } from 'fs/promises';
import { join } from 'path';

/**
 * We have to manipulate the types as long as https://github.com/dfinity/sdk/discussions/2761 is not implemented
 */
const cleanTypes = async ({ dest = `./src/declarations` }) => {
	const promises = readdirSync(dest).map(
		(dir) =>
			new Promise(async (resolve) => {
				const indexPath = join(dest, dir, 'index.js');

				if (!existsSync(indexPath)) {
					resolve();
					return;
				}

				const content = await readFile(indexPath, 'utf-8');
				const clean = content
					.replace(/export const \w* = createActor\(canisterId\);/g, '')
					.replace(/export const canisterId = process\.env\.\w*_CANISTER_ID;/g, '');

				await writeFile(indexPath, clean, 'utf-8');

				resolve();
			})
	);

	await Promise.all(promises);
};

const renameFactory = async ({ dest = `./src/declarations` }) => {
	const promises = readdirSync(dest).map(
		(dir) =>
			new Promise(async (resolve) => {
				const factoryPath = join(dest, dir, `${dir}.did.js`);
				const formattedPath = join(dest, dir, `${dir}.factory.did.js`);

				if (!existsSync(factoryPath)) {
					resolve();
					return;
				}

				await rename(factoryPath, formattedPath);

				resolve();
			})
	);

	await Promise.all(promises);
};

(async () => {
	try {
		await cleanTypes({});

		await renameFactory({});

		console.log(`Types declarations copied!`);
	} catch (err) {
		console.error(`Error while copying the types declarations.`, err);
	}
})();

Once this in place and assuming the types find place in src/declarations I can use these in my libs and apps in TypeScript.

e.g. creating an actor for the Cmc canister becomes:

import type { _SERVICE as CMCActor } from '$declarations/cmc/cmc.did';
import { idlFactory as idlFactorCMC } from '$declarations/cmc/cmc.factory.did';
etc.

export const getCMCActor = async (): Promise<CMCActor> => {
	// Canister IDs are automatically expanded to .env config - see vite.config.ts
	const canisterId = import.meta.env.VITE_IC_CMC_CANISTER_ID;

	const agent = await getAgent({ identity: new AnonymousIdentity() });

	return Actor.createActor(idlFactorCMC, {
		agent,
		canisterId
	});
};

Importing the types work then through the .did files as well. e.g. from one of my custom canister:

import type { Doc } from '$declarations/satellite/satellite.did';

Does that help?


Update: I shoud add that $declarations/ is my relative path to declaration which I set in tsconfig to avoid having ../../../etc everywhere.

It works with SvelteKit, not sure it works out of the box with tsc.

{
	"extends": "./.svelte-kit/tsconfig.json",
	"compilerOptions": {
		...
		"paths": {
			"$lib": ["src/frontend/src/lib"],
			"$lib/*": ["src/frontend/src/lib/*"],
			"$declarations": ["src/declarations"],
			"$declarations/*": ["src/declarations/*"]
		}
	}
}
2 Likes