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

This is just a basic configuration issue but I can’t make it work as I’m new to typescript.

As dfx creates .d.ts files I’d like to use these types directly without importing them to each file in my src folder. From what I understand that’s the point of .d.ts files. But I can’t tell typescript to consider the “did.d.ts” in the declarations folder apparently. From what I have read this can have to do with the index files in the declarations/canister/ folder but I’m not sure.
It also seems like when I have any imports in a d.ts file I can’t use it anymore that way.

It would be helpful to know how you work with the types generated by dfx?

Thanks in advance!

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

You can accomplish the main changes of preventing the default export and avoiding the canisterId issue through dfx.json configs.

"example": {
  "declarations": {
    "node_compatibility": true,
     "env_override": ""
  }
}
2 Likes

That is neat, did not knew that. Is that documented somewhere?

currently mixed between dfx commands docs and Release Notes as the features came out - we really need to review the dfx documentation for comprehensiveness

node_compatiblity was announced here: Internet Computer Content Validation Bootstrap

env_override is documented here: Internet Computer Content Validation Bootstrap

4 Likes

Thanks Kyle! I’ll give it a try tomorrow.

Thank you very much to both of you, that is very helpful! I’ll try that out in the coming days.

1 Like

Thanks it works fine!

env_override feels like a workaround though, it would be cleaner to have a way to skip it entirely. If you ever have a bit of time and want to improve it, not against :wink:

I want it to be better, but it’s hard to weigh the tradeoffs between adding new options or rethinking the design in the first place. Maybe it would be better disable the index entirely, or to allow people to use a plugin or a template to replace it

What I meant is the following definition:

env_override: string | boolean = true

so that developers can pass either false to skip entirely the env or a string to provide a custom value.

Oh, that’s fairly simple

1 Like