Initialize Once
Call initialize() once and reuse the reactor. Metadata generation happens
at initialization.
The MetadataDisplayReactor is a powerful system for working with Internet Computer (IC) canisters that provides runtime metadata generation for building dynamic forms and rendering results. It enables you to create form interfaces that adapt to any canister’s Candid interface without compile-time type definitions.
MetadataDisplayReactor solves a fundamental challenge: How do you build a UI for a canister when you don’t know its interface at compile time?
It provides:
DisplayReactor (from @ic-reactor/core) └── CandidDisplayReactor (display-reactor.ts) └── MetadataDisplayReactor (metadata-display-reactor.ts)bigint → string, Principal → string for easy UI handlingArgumentsMeta)When you call getInputMeta(methodName), you receive comprehensive metadata for building input forms:
interface ArgumentsMeta<A, Name> { functionType: "query" | "update" functionName: Name args: FieldNode[] // One FieldNode per argument defaults: unknown[] // Default values for form initialization schema: z.ZodTuple // Zod schema for validation argCount: number isEmpty: boolean // True if method takes no arguments}FieldNode)Each FieldNode contains everything needed to render a form field:
interface FieldNode { type: VisitorDataType // "record" | "variant" | "text" | "number" | etc. label: string // Raw Candid label (e.g., "__arg0", "owner") displayLabel: string // Human-readable label (e.g., "Arg 0", "Owner") name: string // TanStack Form path (e.g., "[0]", "[0].owner") component: FieldComponentType // Suggested component renderHint: RenderHint // UI hints defaultValue: unknown // Initial form value schema: z.ZodTypeAny // Zod validation schema candidType: string // Original Candid type name // Plus type-specific extras (fields, options, etc.)}| Type | Component | Description |
|---|---|---|
record |
record-container |
Nested object with named fields |
variant |
variant-select |
Discriminated union (like enum with data) |
tuple |
tuple-container |
Fixed-length array |
optional |
optional-toggle |
Nullable field |
vector |
vector-list |
Dynamic array |
blob |
blob-upload |
Binary data (vec nat8) |
recursive |
recursive-lazy |
Self-referential type |
principal |
principal-input |
IC Principal |
text |
text-input |
String, and nat, int, nat64, int64 |
number |
number-input |
Float, or an integer of up to 32 bits |
boolean |
boolean-checkbox |
Boolean |
null |
null-hidden |
Null type |
nat, int, nat64 and int64 arguments are text fields, because a
JavaScript number cannot hold every value: the form keeps them as decimal
strings. Their candidType names the integer type, and their inputProps set
inputMode: "numeric" and a digits-only pattern.
Each field type includes additional properties relevant to its nature:
interface RecordField { type: "record" fields: FieldNode[] // Child fields defaultValue: Record<string, unknown> // Object default}interface VariantField { type: "variant" options: FieldNode[] // Option fields defaultOption: string // First option key defaultValue: Record<string, unknown> // Selected option value getOptionDefault(option: string): Record<string, unknown> getOption(option: string): FieldNode getSelectedKey(value): string getSelectedOption(value): FieldNode}interface VectorField { type: "vector" itemField: FieldNode // Template for items defaultValue: unknown[] // Empty array getItemDefault(): unknown // New item default createItemField(index: number): FieldNode // Create indexed field}interface OptionalField { type: "optional" innerField: FieldNode // Inner type field defaultValue: null // Always null getInnerDefault(): unknown // Get inner default when enabling isEnabled(value): boolean // Check if value is set}interface NumberField { type: "number" // float32/64, or nat8-32 and int8-32 unsigned: boolean // nat8-32 vs int8-32 isFloat: boolean // float32/64 bits?: number // 8, 16, 32, or 64 for float64 min?: string // Min value as string max?: string // Max value as string format: NumberFormat // "timestamp" | "cycle" | "normal" inputProps: PrimitiveInputProps}interface TextField { type: "text" // text, and nat, int, nat64, int64 as decimal strings minLength?: number maxLength?: number multiline?: boolean format: TextFormat // "email" | "url" | "phone" | "plain" | etc. inputProps: PrimitiveInputProps}A plain text field’s schema takes any string, "" included: Candid text
has no required-ness, and a method may take an empty one. A text field whose
label gives it a format (see Format Detection) keeps that
format’s check and rejects "", the numbers held as text still need
digits, and a func reference still needs its method name.
MethodMeta)When you call getOutputMeta(methodName), you receive metadata for rendering results:
interface MethodMeta<A, Name> { functionType: FunctionType functionName: Name returns: ResultNode[] // Schema for return values returnCount: number resolve(data): MethodResult<A> // Transform raw → display}ResultNode)interface ResultNode { type: VisitorDataType // Same types as arguments label: string // Field label displayLabel: string // Human-readable label candidType: string // Original Candid type displayType: DisplayType // "string" | "number" | "object" | "array" | etc. resolve(data): ResolvedNode // Transform raw value}| DisplayType | Description |
|---|---|
string |
Text, Principal, large numbers (as string) |
number |
Small integers, floats |
boolean |
Boolean values |
null |
Null values |
object |
Records |
array |
Vectors, Tuples |
variant |
Variant types |
variant-null |
Variants whose options carry no data, like variant { Active; Paused } |
result |
Special case for Ok/Err variants |
nullable |
Optional types |
recursive |
Self-referential types; the value is in inner |
func |
Function references (canister ID + method) |
func-record |
A record holding a callback (func reference) and its arguments |
blob |
Not produced; blobs resolve as string (below) |
unknown |
Types with no display form, such as reserved |
Blob nodes keep type: "blob" but resolve with displayType: "string" — the
value is always a hex string, and length is the byte count. Key any
blob-specific rendering (byte count, hash summary) on node.type.
The resolve() method transforms raw canister responses into display-ready structures:
import { MetadataDisplayReactor } from "@ic-reactor/candid"
const reactor = new MetadataDisplayReactor({ canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", // ICP Ledger clientManager, name: "ICPLedger",})
async function resolveTransfer() { await reactor.initialize()
// Raw canister response const rawResult = { Ok: 1000000000n } // bigint
// Get metadata (undefined for a method the service does not have) const meta = reactor.getOutputMeta("icrc1_transfer")
// Resolve to display types const resolved = meta?.resolve(rawResult) // resolved = { // functionType: "update", // functionName: "icrc1_transfer", // results: [{ // type: "variant", // selected: "Ok", // selectedValue: { // The resolved value of the selected option // type: "number", // value: "1000000000", // String, not bigint! // displayType: "string" // }, // raw: { Ok: 1000000000n } // }], // raw: { Ok: 1000000000n } // }}Candid func types can appear as data fields in records, commonly seen in callback references:
import { Principal } from "@icp-sdk/core/principal"
// Raw canister response with func referenceconst rawResult = { callback: [ Principal.fromText("sa4so-piaaa-aaaar-qacnq-cai"), "get_transactions", ],}
// The callback field is resolved to:// {// type: "func",// displayType: "func",// canisterId: "sa4so-piaaa-aaaar-qacnq-cai",// methodName: "get_transactions",// raw: [Principal, "get_transactions"]// }defaultArgsA record with exactly one func field, such as an archived_transactions
entry returned by a ledger’s get_transactions, resolves as a funcRecord
node (displayType: "func-record"). Besides canisterId, methodName and
funcClass, it carries defaultArgs: the values beside the callback,
arranged as the callback’s arguments. When the
callback takes exactly the type of the one other field (ICRC-3’s
{ args; callback }), that field is the argument.
import { MetadataDisplayReactor } from "@ic-reactor/candid"
// `archived` is the resolved funcRecord nodeconst archive = new MetadataDisplayReactor({ name: "archive", canisterId: archived.canisterId, clientManager, funcClass: { methodName: archived.methodName, func: archived.funcClass },})
await archive.callMethod({ functionName: archived.methodName, args: archived.defaultArgs, // [{ start: "0", length: "100" }]})defaultArgs are in the type space of the reactor that resolved the record,
which is the one its callMethod takes: display types on
MetadataDisplayReactor, and Candid values on MetadataReactor
([{ start: 0n, length: 100n }], with a Principal for a principal and []
or [x] for an opt). Build the callback’s reactor with the same class.
The system automatically detects formats from field labels:
| Format | Detected From |
|---|---|
timestamp |
created_at, updated_at, timestamp_nanos, etc. |
email |
email, mail |
url |
url, link, website |
phone |
phone, tel, mobile |
uuid |
uuid, guid |
btc |
btc, bitcoin |
eth |
eth, ethereum |
principal |
principal, canister |
account-id |
account_identifier, ledger_account |
| Format | Detected From |
|---|---|
timestamp |
time, date, created_at, etc. |
cycle |
cycle, cycles |
normal |
Everything else |
Create the reactor with new, then initialize it:
import { ClientManager } from "@ic-reactor/core"import { MetadataDisplayReactor } from "@ic-reactor/candid"import { QueryClient } from "@tanstack/query-core"
// 1. Create client managerconst clientManager = new ClientManager({ queryClient: new QueryClient(),})
await clientManager.initialize()
// 2. Create reactorconst reactor = new MetadataDisplayReactor({ canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", // ICP Ledger clientManager, name: "ICPLedger",})
// 3. Initialize (fetches Candid from canister)await reactor.initialize()
// 4. Get available methodsconst methods = reactor.getMethodNames()console.log(methods) // ["icrc1_balance_of", "icrc1_transfer", ...]import { MetadataDisplayReactor } from "@ic-reactor/candid"
const reactor = new MetadataDisplayReactor({ canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", // ICP Ledger clientManager, name: "ICPLedger",})
async function describeTransferForm() { await reactor.initialize()
// Get input metadata for a method const inputMeta = reactor.getInputMeta("icrc1_transfer") if (!inputMeta) throw new Error("icrc1_transfer is not in the service")
console.log(inputMeta.functionName) // "icrc1_transfer" console.log(inputMeta.functionType) // "update" console.log(inputMeta.argCount) // 1 console.log(inputMeta.defaults) // [{ to: { owner: "", subaccount: null }, ... }]
// Iterate over args to build form for (const field of inputMeta.args) { console.log(field.type) // "record" console.log(field.displayLabel) // "Arg 0" console.log(field.component) // "record-container"
// For records, access nested fields if (field.type === "record") { for (const childField of field.fields) { console.log(childField.name) // "[0].to", "[0].amount", etc. console.log(childField.displayLabel) // "To", "Amount", etc. } } }}DynamicFieldInput is the field renderer of the next section. The metadata
exists once the reactor has initialized, so load it before the form renders, in
a route loader or as below. The form is created with a hook, so it lives in a
component that takes the metadata as a prop.
import { MetadataDisplayReactor } from "@ic-reactor/candid"import { useForm } from "@tanstack/react-form"
const reactor = new MetadataDisplayReactor({ canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", // ICP Ledger clientManager, name: "ICPLedger",})
// Get metadataasync function loadTransferMeta() { await reactor.initialize()
const inputMeta = reactor.getInputMeta("icrc1_transfer") if (!inputMeta) throw new Error("icrc1_transfer is not in the service") return inputMeta}
type TransferMeta = Awaited<ReturnType<typeof loadTransferMeta>>
function DynamicForm({ inputMeta }: { inputMeta: TransferMeta }) { const { args, defaults, schema } = inputMeta
// Create form with metadata const form = useForm({ defaultValues: defaults, validators: { onBlur: schema, // Zod schema }, onSubmit: async ({ value }) => { const result = await reactor.callMethod({ functionName: "icrc1_transfer", args: value, // Use form values directly }) console.log(result) }, })
// Render fields dynamically. Argument `i` is at the path `[i]`. return ( <form onSubmit={(e) => { e.preventDefault() void form.handleSubmit() }} > {args.map((field, index) => ( <form.Field key={index} name={`[${index}]`}> {(fieldApi) => ( <DynamicFieldInput field={field} form={form} fieldApi={fieldApi} /> )} </form.Field> ))} <button type="submit">Call Method</button> </form> )}Nested fields are rendered with TanStack Form’s Field component, addressed by
the child’s own name: the full form path ([0].to, [0].amount).
import { isFieldType, type FieldNode } from "@ic-reactor/candid"import { Field, type AnyFieldApi, type AnyFormApi } from "@tanstack/react-form"
function DynamicFieldInput({ field, form, fieldApi,}: { field: FieldNode form: AnyFormApi fieldApi: AnyFieldApi}) { // Handle compound types if (isFieldType(field, "record")) { return ( <fieldset> <legend>{field.displayLabel}</legend> {field.fields.map((childField) => ( <Field key={childField.name} form={form} name={childField.name}> {(childApi) => ( <DynamicFieldInput field={childField} form={form} fieldApi={childApi} /> )} </Field> ))} </fieldset> ) }
if (isFieldType(field, "variant")) { const selectedKey = field.getSelectedKey(fieldApi.state.value) const selectedOption = field.getSelectedOption(fieldApi.state.value) return ( <div> <label>{field.displayLabel}</label> <select value={selectedKey} onChange={(e) => { fieldApi.handleChange(field.getOptionDefault(e.target.value)) }} > {field.options.map((opt) => ( <option key={opt.label} value={opt.label}> {opt.displayLabel} </option> ))} </select> {/* Render selected option's field */} {selectedOption.type !== "null" && ( <Field form={form} name={selectedOption.name}> {(optionApi) => ( <DynamicFieldInput field={selectedOption} form={form} fieldApi={optionApi} /> )} </Field> )} </div> ) }
if (isFieldType(field, "vector")) { const items: unknown[] = fieldApi.state.value || [] return ( <div> <label>{field.displayLabel}</label> {items.map((_, i) => { const itemField = field.createItemField(i) return ( <Field key={itemField.name} form={form} name={itemField.name}> {(itemApi) => ( <DynamicFieldInput field={itemField} form={form} fieldApi={itemApi} /> )} </Field> ) })} <button type="button" onClick={() => fieldApi.pushValue(field.getItemDefault())} > Add Item </button> </div> ) }
if (isFieldType(field, "optional")) { const isEnabled = field.isEnabled(fieldApi.state.value) return ( <div> <label> <input type="checkbox" checked={isEnabled} onChange={(e) => { fieldApi.handleChange( e.target.checked ? field.getInnerDefault() : null ) }} /> {field.displayLabel} </label> {isEnabled && ( <Field form={form} name={field.innerField.name}> {(innerApi) => ( <DynamicFieldInput field={field.innerField} form={form} fieldApi={innerApi} /> )} </Field> )} </div> ) }
// Primitive types. nat, int, nat64 and int64 are text fields. if (isFieldType(field, "text") || isFieldType(field, "principal")) { return ( <div> <label>{field.displayLabel}</label> <input {...field.inputProps} value={fieldApi.state.value ?? ""} onChange={(e) => fieldApi.handleChange(e.target.value)} /> {field.format === "timestamp" && field.candidType === "nat64" && ( <small>Timestamp (nanoseconds)</small> )} </div> ) }
if (isFieldType(field, "number")) { return ( <div> <label>{field.displayLabel}</label> <input {...field.inputProps} value={fieldApi.state.value ?? ""} onChange={(e) => fieldApi.handleChange(e.target.value)} /> </div> ) }
if (isFieldType(field, "boolean")) { return ( <label> <input type="checkbox" checked={fieldApi.state.value ?? false} onChange={(e) => fieldApi.handleChange(e.target.checked)} /> {field.displayLabel} </label> ) }
return <span>Unsupported type: {field.type}</span>}import { MetadataDisplayReactor } from "@ic-reactor/candid"
const reactor = new MetadataDisplayReactor({ canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", // ICP Ledger clientManager, name: "ICPLedger",})
async function showBalance() { await reactor.initialize()
// Call method and get resolved result const result = await reactor.callMethod({ functionName: "icrc1_balance_of", args: [{ owner: "aaaaa-aa" }], })
// result is already resolved through transformResult console.log(result) // { // functionType: "query", // functionName: "icrc1_balance_of", // results: [{ type: "number", value: "1000000000", displayType: "string", raw: 1000000000n }], // raw: 1000000000n // }
// The resolved result keeps the raw canister value, so you can resolve it // again yourself with the same metadata const outputMeta = reactor.getOutputMeta("icrc1_balance_of") const resolved = outputMeta?.resolve(result.raw)}Switch on a node’s type: it narrows the node to the shape that has fields,
items or selectedValue, and unlike displayType it tells a blob from the
other strings.
import type { MethodResult, ResultNode } from "@ic-reactor/candid"
function ResultDisplay({ resolved }: { resolved?: MethodResult }) { if (!resolved) return null
return ( <div> <h3> {resolved.functionName} ({resolved.functionType}) </h3> {resolved.results.map((node, i) => ( <ResultNodeView key={i} node={node} /> ))} </div> )}
function ResultNodeView({ node }: { node: ResultNode }) { switch (node.type) { case "blob": // A blob resolves with displayType "string": the value is always a hex // string, whatever the payload size, so render the byte count instead. return ( <div> <strong>{node.label}:</strong> <code> {node.length} bytes (hash: {node.hash.slice(0, 16)}...) </code> </div> )
case "text": case "principal": case "number": return ( <div> <strong>{node.label}:</strong> {node.value} </div> )
case "boolean": return ( <div> <strong>{node.label}:</strong> {node.value ? "Yes" : "No"} </div> )
case "null": return ( <div> <strong>{node.label}:</strong> <em>null</em> </div> )
case "record": return ( <div> <strong>{node.label}:</strong> <div style={{ marginLeft: 20 }}> {Object.entries(node.fields).map(([key, field]) => ( <ResultNodeView key={key} node={field} /> ))} </div> </div> )
case "tuple": case "vector": return ( <div> <strong>{node.label}:</strong> <ul> {node.items.map((item, i) => ( <li key={i}> <ResultNodeView node={item} /> </li> ))} </ul> </div> )
case "variant": // Also a `result` (Ok/Err) and a variant whose options carry no data return ( <div> <strong>{node.label}:</strong> <em>{node.selected}</em> <div style={{ marginLeft: 20 }}> <ResultNodeView node={node.selectedValue} /> </div> </div> )
case "optional": if (node.value === null) { return ( <div> <strong>{node.label}:</strong> <em>not set</em> </div> ) } return <ResultNodeView node={node.value} />
case "func": return ( <div> <strong>{node.label}:</strong> <span> Callback: {node.canisterId}.{node.methodName}() </span> </div> )
case "recursive": // A recursive type's value, resolved one level down return <ResultNodeView node={node.inner} />
default: // funcRecord and unknown. `raw` is the Candid value, and JSON.stringify // throws on a bigint unless a replacer converts it. return ( <div> <strong>{node.label}:</strong> <pre> {JSON.stringify( node.raw, (_key, value) => typeof value === "bigint" ? value.toString() : value, 2 )} </pre> </div> ) }}You can register methods at runtime with Candid signatures:
import { MetadataDisplayReactor } from "@ic-reactor/candid"
const reactor = new MetadataDisplayReactor({ canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", // ICP Ledger clientManager, name: "ICPLedger",})
async function callCustomMethod() { await reactor.initialize()
// Register a method not in the original service await reactor.registerMethod({ functionName: "custom_method", candid: "(record { input: text }) -> (record { output: text }) query", })
// Metadata is automatically regenerated const argMeta = reactor.getInputMeta("custom_method") const resultMeta = reactor.getOutputMeta("custom_method")
// Call the newly registered method const result = await reactor.callMethod({ functionName: "custom_method", args: [{ input: "hello" }], })}Combine registration and calling in one step:
import { MetadataDisplayReactor } from "@ic-reactor/candid"
const reactor = new MetadataDisplayReactor({ canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", // ICP Ledger clientManager, name: "ICPLedger",})
async function callWithMeta() { await reactor.initialize()
const { result, meta } = await reactor.callDynamicWithMeta({ functionName: "get_info", candid: "() -> (record { name: text; version: nat }) query", args: [], })
// meta contains the result metadata // result is already resolved}The library provides utility functions for working with fields:
import { isFieldType, isCompoundField, isPrimitiveField, hasChildFields, hasOptions, formatLabel,} from "@ic-reactor/candid"
// Type guardsif (isFieldType(field, "record")) { // field is typed as RecordField field.fields.forEach((child) => console.log(child.name))}
if (isCompoundField(field)) { // field is record | variant | tuple | optional | vector | recursive}
if (isPrimitiveField(field)) { // field is principal | number | text | boolean | null}
if (hasChildFields(field)) { // field has .fields array (record, tuple)}
if (hasOptions(field)) { // field has .options array (variant)}
// Label formattingformatLabel("__arg0") // "Arg 0"formatLabel("_0_") // "Item 0"formatLabel("created_at") // "Created At"formatLabel("userAddress") // "User Address"MetadataDisplayReactor is fully typed. When you have a typed actor, the generics flow through:
import { MetadataDisplayReactor } from "@ic-reactor/candid"import { canisterId, type _SERVICE } from "./declarations/my_canister"
const reactor = new MetadataDisplayReactor<_SERVICE>({ canisterId, clientManager, name: "MyCanister",})
// Method names are type-checkedconst meta = reactor.getInputMeta("my_method") // TypeScript knows the method names
// Return types are inferredconst result = await reactor.callMethod({ functionName: "my_method", args: ["hello", "42"], // display types: the bigint is a string})Here’s a complete example that creates a UI for any canister:
import { ClientManager } from "@ic-reactor/core"import { MetadataDisplayReactor, isFieldType } from "@ic-reactor/candid"import { QueryClient } from "@tanstack/query-core"
async function createCanisterExplorer(canisterId: string) { // Setup const clientManager = new ClientManager({ queryClient: new QueryClient(), }) await clientManager.initialize()
const reactor = new MetadataDisplayReactor({ canisterId, clientManager, name: "Explorer", })
await reactor.initialize()
// Get all methods const methods = reactor.getMethodNames()
// Build method info const methodInfo = methods.map((name) => { const inputMeta = reactor.getInputMeta(name) const outputMeta = reactor.getOutputMeta(name)
return { name, type: inputMeta?.functionType, argCount: inputMeta?.argCount ?? 0, returnCount: outputMeta?.returnCount ?? 0, defaultValues: inputMeta?.defaults, schema: inputMeta?.schema, } })
// Example: Call a query method async function callMethod(methodName: string, args: unknown[]) { const result = await reactor.callMethod({ functionName: methodName, args, }) return result }
return { methods: methodInfo, getInputMeta: (name: string) => reactor.getInputMeta(name), getOutputMeta: (name: string) => reactor.getOutputMeta(name), callMethod, }}
// Usageconst explorer = await createCanisterExplorer("ryjl3-tyaaa-aaaaa-aaaba-cai")
console.log("Available methods:", explorer.methods)
// Get form metadata for icrc1_balance_ofconst balanceInputMeta = explorer.getInputMeta("icrc1_balance_of")console.log("Form fields:", balanceInputMeta?.args)
// Call with display types (strings)const balance = await explorer.callMethod("icrc1_balance_of", [ { owner: "aaaaa-aa" },])console.log("Balance:", balance)Initialize Once
Call initialize() once and reuse the reactor. Metadata generation happens
at initialization.
Use Type Guards
Use isFieldType() and other helpers for type-safe field handling.
Leverage Defaults
Use defaultValues from metadata to initialize forms properly.
Use Zod Schemas
The built-in schemas integrate with most form libraries for validation.
| Method | Description |
|---|---|
initialize() |
Parse Candid and generate metadata |
getMethodNames() |
Get list of available method names |
getInputMeta(name) |
Get input form metadata for a method, or undefined |
getOutputMeta(name) |
Get output display metadata for a method, or undefined |
getAllInputMeta() |
Get input metadata for all methods |
getAllOutputMeta() |
Get output metadata for all methods |
registerMethod(options) |
Register a dynamic method |
callMethod(options) |
Call a method with display type transformation |
callDynamicWithMeta(options) |
Register, call, and return metadata |
| Property | Type | Description |
|---|---|---|
type |
VisitorDataType |
Field type identifier |
label |
string |
Raw Candid label |
displayLabel |
string |
Human-readable label |
name |
string |
Form path (TanStack compatible) |
component |
FieldComponentType |
Suggested component |
renderHint |
RenderHint |
UI rendering hints |
defaultValue |
unknown |
Initial value |
schema |
z.ZodTypeAny |
Validation schema |
candidType |
string |
Original Candid type |
| Property | Type | Description |
|---|---|---|
type |
VisitorDataType |
Node type identifier |
label |
string |
Field label |
displayLabel |
string |
Human-readable label |
candidType |
string |
Original Candid type |
displayType |
DisplayType |
Display category |
resolve(data) |
Function |
Transform raw to display |