Type Safety
IC Reactor provides end-to-end type safety from your Candid definitions all the way to your React components. This guide explains how the type system works and how to get the most out of it.
How It Works
Section titled “How It Works”Candid IDL → dfx generate → TypeScript Types → IC Reactor → React Hooks- Candid IDL — Your canister’s interface definition (
.didfile) - dfx generate — Creates TypeScript declarations from Candid
- IC Reactor — Infers types for all hooks and methods
- React Components — Full autocomplete and type checking
Generated Types
Section titled “Generated Types”When you run dfx generate, it creates TypeScript files in src/declarations/<canister_name>/:
import type { Principal } from "@icp-sdk/core/principal"
export interface User { id: string name: string email: string createdAt: bigint}
export interface CreateUserInput { name: string email: string}
export type CreateUserError = | { EmailAlreadyExists: null } | { InvalidEmail: null }
export type Result = { Ok: User } | { Err: CreateUserError }
export interface _SERVICE { getUser: (id: string) => Promise<[] | [User]> createUser: (input: CreateUserInput) => Promise<Result> listUsers: (page: bigint, limit: bigint) => Promise<User[]>}Reactor Setup
Section titled “Reactor Setup”Pass the _SERVICE type to your Reactor for full type inference:
import { Reactor } from "@ic-reactor/core"import { idlFactory, type _SERVICE } from "./declarations/backend"
// TypeScript knows the exact shape of _SERVICEconst backend = new Reactor<_SERVICE>({ clientManager, idlFactory, canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",})Query Type Inference
Section titled “Query Type Inference”The useActorQuery hook infers types automatically:
const { data } = useActorQuery({ functionName: "getUser", // ✅ Autocomplete shows valid methods args: ["user-123"], // ✅ Type checked as [string]})// data is typed as User | undefined (from [] | [User])Autocomplete
Section titled “Autocomplete”Your IDE will show:
- All available method names
- Required argument types
- Return type for each method
Compile-Time Safety
Section titled “Compile-Time Safety”TypeScript catches errors before runtime:
// ❌ TypeScript error: 'invalidMethod' does not exist on _SERVICEuseActorQuery({ functionName: "invalidMethod", args: [],})
// ❌ TypeScript error: Argument of type 'number' is not assignable to 'string'useActorQuery({ functionName: "getUser", args: [123], // Wrong type!})
// ✅ CorrectuseActorQuery({ functionName: "getUser", args: ["user-123"],})Mutation Type Inference
Section titled “Mutation Type Inference”Mutations are equally type-safe:
const { mutate } = useActorMutation({ functionName: "createUser", // ✅ Autocomplete})
// ✅ Type-safe argumentsmutate([{ name: "Alice", email: "alice@example.com" }])
// ❌ TypeScript error: missing 'email' fieldmutate([{ name: "Alice" }])Select Transformations
Section titled “Select Transformations”The select option maintains type safety:
// data is User | undefinedconst { data } = useActorQuery({ functionName: "getUser", args: [userId],})
// data is string | undefined (just the name)const { data } = useActorQuery({ functionName: "getUser", args: [userId], select: (user) => user?.name, // ✅ user is typed as User | undefined})Error Types
Section titled “Error Types”Error types are also inferred:
const { error } = useActorQuery({ functionName: "getUser", args: [userId],})// error is typed as CanisterError | CallError | unknownFor mutations with specific error types:
const { error } = useActorMutation({ functionName: "createUser", onError: (error) => { if (error instanceof CanisterError) { // error.err is typed as CreateUserError if ("EmailAlreadyExists" in error.err) { toast.error("Email already in use") } } },})Display Types (Auto Transformations)
Section titled “Display Types (Auto Transformations)”With DisplayReactor, types are automatically transformed:
| Candid Type | Raw TypeScript | Display (transformed) |
|---|---|---|
nat | bigint | string |
int | bigint | string |
nat64 | bigint | string |
principal | Principal | string |
blob | Uint8Array | hexString, Uint8Array |
opt T | [T] | [] | T | null |
Using DisplayReactor
Section titled “Using DisplayReactor”import { DisplayReactor } from "@ic-reactor/core"import { createActorHooks } from "@ic-reactor/react"
// Create a DisplayReactor directlyconst reactor = new DisplayReactor<_SERVICE>({ clientManager, idlFactory, canisterId: "...",})
// Pass it to createActorHooks - types are automatically inferred as "display"const { useActorQuery } = createActorHooks(reactor)
// Inputs and outputs use display typesconst { data } = useActorQuery({ functionName: "getBalance", args: ["aaaaa-aa"], // Principal as string})// data is string instead of bigintType Assertions
Section titled “Type Assertions”With display types, TypeScript knows the transformed shapes:
const { data: balance } = useActorQuery({ functionName: "getBalance", args: [principalString],})// balance is string, not bigint - can be used directly in UI!
return <span>Balance: {balance}</span>Result Unwrapping
Section titled “Result Unwrapping”IC Reactor automatically unwraps Result types:
// Canister method: createUser(input) -> Result<User, CreateUserError>
// IC Reactor:// - On Ok: returns the User value// - On Err: throws CanisterError with the error variant
const { data } = useActorQuery({ functionName: "createUser", args: [input],})// data is User (not Result<User, Error>)The error type is available in the error property:
const { data, error } = useActorQuery({ functionName: "createUser", args: [input],})// data: User | undefined// error: CanisterError<CreateUserError> | CallError | undefinedOptional Unwrapping
Section titled “Optional Unwrapping”Optional types (opt T) are transformed too:
// Candid: getUser(id) -> opt User (returns [] | [User])
// With display types:const { data } = useActorQuery({ functionName: "getUser", args: [userId],})// data is User | null (easier to work with than [] | [User])Type Narrowing
Section titled “Type Narrowing”Use type narrowing for safer access:
function UserProfile({ userId }: { userId: string }) { const { data, isPending, isError, error } = useActorQuery({ functionName: "getUser", args: [userId], })
if (isPending) return <Loading />
if (isError) return <Error error={error} />
// TypeScript knows data is defined here return ( <div> <h1>{data.name}</h1> {/* ✅ No null check needed */} <p>{data.email}</p> </div> )}Generic Patterns
Section titled “Generic Patterns”Create type-safe reusable patterns:
// Type-safe query factoryfunction useUser(userId: string) { return useActorQuery({ functionName: "getUser", args: [userId], enabled: !!userId, })}
// Type-safe mutation factoryfunction useCreateUser() { return useActorMutation({ functionName: "createUser", })}
// Usageconst { data: user } = useUser(userId) // user: User | undefinedconst { mutate } = useCreateUser() // mutate expects CreateUserInputBest Practices
Section titled “Best Practices”- Always use the generated
_SERVICEtype — Pass it toReactor<_SERVICE> - Enable strict TypeScript — Use
strict: truein tsconfig.json - Trust the types — If it compiles, the call is structurally valid
- Run dfx generate after Candid changes — Keep types in sync
- Use type narrowing — Check for
isPendingandisErrorbefore accessingdata
TypeScript Configuration
Section titled “TypeScript Configuration”Ensure your tsconfig.json is properly configured:
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "moduleResolution": "bundler", "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"] }}Further Reading
Section titled “Further Reading”- Queries — Type-safe query patterns
- Mutations — Type-safe mutation patterns
- Error Handling — Type-safe error handling