Type creation
Circling back to metadata. There are two important things to remember when using the API to make queries or send transactions.
The functionality available, e.g. exposed on
api.query.*
is not hard-coded in the API, rather this is decorated from the chain metadata. So the metadata lets the API know which endpoints are available and what the type for those endpoints are.When you supply a value to the API, internally it will convert that value to the correct type as expected by the chain, i.e. as determined by the metadata. This means that a function such as
balances.transfer(address: Address, value: Balance)
can take at least the following inputs, which are all converted to the correct types -address
can be anAddress
, anAccountId
, anUint8Array
publicKey, a hex publicKey or an ss58 formatted address;value
can be aBalance
, a value encoded in hex, aBN
object, a base-10 string, a JSnumber
, a JSBigInt
or even a SCALE-encodedUint8Array
It is advised to not supply a
api.tx.somewhere.something
,api.query.somewhere.something
etc. call withCodec
types created viacreateType
, but to simply apply the value. This will ensure that if theCodec
type needed for a certain call changes given a specific runtime, then the API will be able to resolve that type for you. This ensures the minimum amount of maintence, and refactoring required when changes to the type naming is applied.
// The following is not advised
const something = api.createType('SomeType', { foo: 'bar' });
...
await api.tx.somewhere.something(something);
...
// This following is advised
await api.tx.somewhere.something({ foo: 'bar' });
In cases where a value is returned such as storage queries, the response from the chain is always encoded into the correct Codec
type. This means that while the node may return an encoded block (with encoded extrinsics) via api.rpc.chain.getBlock()
, this is decoded into a proper SignedBlock
by the API. Outputting this value via .toJSON()
will yield an encoding for RPC, so if you are not using TypeScript (which adds code helpers on decoded objects), a representation via .toHuman()
will be more representative of the actual object fields, re-formatted for human consumption.
Why create types
With the conversions done in the API, there are limited reasons to create types "manually". However, just because there are not thousands of reasons, does not mean it is not valid. For instance, you may retrieve an Option
and for the sake of sanity would like to use .unwrapOr()
on it, returning a Codec
default value where the value .isNone
.
// type import for TypeScript
import type { Balance } from '@polkadot/types/interfaces';
...
// unwrap out option into a zero Balance when not found
// (This can be done via `.unwrapOrDefault()`, which does the same underlying)
const balance: Balance = balanceOpt.unwrapOr(api.createType('Balance'));
In the example above, we introduced the api.createType(<typeName>, [<value>])
. The same format is also exposed by the TypeRegistry
(more on this in a short while) as well as createType(...)
from the actual @polkadot/types
package. All doing exactly the same.
Choosing how to create
In most cases, you would always want to use the api.createType
helper. What this does is call the underlying @polkadot/types
createType
, passing through the registry that is attached to the API. Registry? Yes, registry.
The registry contains a listing of all internal types and their classes that have been registered. So upon creation of an API instance, a registry
object is attached to the API and this is passed through to all created types. This allows the type definitions to not pollute the global namespace, but rather be contained and able to reference one another.
As mentioned, the createType
functions all do exactly the same, and in 99.99% of the cases you would be recommended to just forget about everything and use api.createType
if and when required. In some cases, you may just have a type object and from that want to create another type instance. For that you can access the registry
on the type object and call createType
on it. (If this type object was created from an API instance, the registry on the type and on the API will point to the same instance.)
Basically, this means that we have equivalency in creation for all the items below, all creating on the same registry (containing all injected types), and all wrapping the same value -
import { createType } from '@polkadot/types';
// via API (recommended)
api.createType('Balance', 123);
// via registry (`.registry` is on all API and Codec objects)
api.registry.createType('Balance', 123n);
// via the low-level approach (not recommended)
createType(api.registry, 'Balance', '123');
How to create types
As all methods of creation basically expose the same API, we will explain only how api.createType
works. If the type is an alias for another type (like 'Balance'
, which is just a u128
), the second parameter of api.createType
is the value of the object you are creating, as detailed in the previous section:
const x = api.createType('Balance', 123);
console.log(`x is equal to ${x.toNumber()}`);
If the type you want to create is a struct, then the second parameter is a dictionary which maps field names to their values:
...
const api = await ApiPromise.create({
types: {
MyStruct: {
a: "u32",
b: "Vec<u32>",
c: "Option<u32>"
}
}
});
const s = api.createType("MyStruct", {a: 1, b: [2, 3], c: 4});
console.log(`s.a == ${s.a.toNumber()}`);
console.log(`s.b == ${s.b.toJSON()}`);
console.log(`s.c == ${s.c.unwrap().toNumber()}`);
If you don't specify a field, it will be initialized with the default value. Numbers are zero, vectors are empty, options are None
by default.
If you want to create an enum, the pattern is api.createType(type, enumerator)
(for C-style enums or if you want to rely on default enumeration) or api.createType(type, {enumerator: value})
(for typed enums). For example:
const api = await ApiPromise.create({
...
types: {
CLikeEnum: {
_enum: ['One', 'Two', 'Three']
},
TypedEnum: {
_enum: {
One: 'Compact<u32>',
Two: 'u64',
Three: 'Option<Balance>',
Four: null
}
}
}
});
const one = api.createType('CLikeEnum', 'One');
console.log(one.isOne); // true
const two = api.createType('TypedEnum', {'Two': 123});
console.log(two.asTwo.toNumber()); // 123
const three = api.createType('TypedEnum', 'Three'); // Default initialization
console.log(three.asThree.isNone); // true
You may want to construct a Call
type given a specific tx. Using create type is unneecessary createType
, and it can be achieved by simply using the method
key attached to a tx
.
const tx = await api.tx.balances.transfer(BOB, 12345);
console.log('Hex = ', tx.method.toHex())
Using with TypeScript
The API is built with TypeScript (as are all projects in the polkadot-js organization and as such allows developers using TS to have access to all the type interfaces defined on the chain, as well as having access to typings on interacting with the api.*
namespaces. In the next section we will provide an overview of what is available in terms of types and TypeScript.