Static Types vs. Run-Time Types on the Flow Blockchain

veganbeef
4 min readMar 3, 2023

--

As a strongly typed language, Cadence requires all Flow blockchain engineers to interact with its type system in some way, even if it’s just to manage and borrow NFT capabilities. However, you might not be aware of all aspects of the Cadence type system. It has layers! And in this blog post, I’m going to explore those layers, from static and run-time types to composite, interface, and restricted types. Let’s get peeling!

Simple and Composite Types

The most common way to use types in any typed language is through simple types. These are your Ints, Strings, Bools, Arrays, etc. However, you can’t do much with just the basics, because you need composability, a key tenet of Flow. Enter composite types! Basically everything else in the Cadence language is a composite type composed of simple types (or other composite types) and functions, aside from contracts.

All composite types stem from AnyStruct and AnyResource, which are the top level types for structs and resources respectively. These types are commonly used when dynamically referring to any struct or resource that implements a certain interface as a restriction, for example &AnyResource{FungibleToken.Receiver} refers to a borrowed fungible token receiver capability from any token vault, regardless of the specific resource type like FlowToken.Vault or FUSD.Vault. However, the AnyStruct and AnyResource keywords can be omitted, so &{FungibleToken.Receiver} means the same thing as the earlier example.

Composite types are often paired with interfaces to restrict functionality resulting in restricted types, like in the examples in the previous paragraph. This type structure allows for the capability-based access control model that’s baked into the Flow blockchain.

Static and Run-Time Types

Now that we’ve discussed some of the syntax conventions for types in Cadence, let’s get abstract!

Everything we’ve discussed so far is a static type, which is an explicitly written type used in the type checking process when a transaction or contract is submitted to a Flow node. Here’s the thing though: run-time types are static types too.

A run-time type is a special static type that serves as a run-time representation of a static type, using the type keyword Type, which stores a reference to a specific struct or resource from a specific contract. Even your intrepid author finds that previous sentence a bit intimidating, so let’s look at an example:

let nftType: Type = CompositeType(userSuppliedTypeString) ?? panic("invalid type")
let nftRef = collectionPublicRef.borrowNFT(id: proposedNft.nftID)
assert(nftRef.getType() == nftType, message: "NFT type mismatch: ".concat(nftType.identifier))

This is code snippet from evaluate’s Swap contract (slightly modified for clarity) where an input type supplied by the user is checked against the borrowed NFT reference type as part of verifying ownership. The variable nftType stores a reference to the NFT type that the user believes they are trading, for example that type might have the identifier ”A.0b2a3299cc857e29.TopShot.NFT”, referring to a TopShot NFT. All NFTs with the same resource name from the same contract will have the same type, regardless of their token ID.

The borrowed reference to the NFT should have the same type as the user-supplied type string, otherwise something fishy might be going on. We check that by using the getType() function, which exists on all values and returns a Type value.

Run-time types are mostly useful for verifying types against some standard, such a user-supplied value or some public resource like NFT Catalog. They cannot, however, be passed as dynamic values where static types are otherwise required, like in the getCapability<>() type definition. If you’re familiar with TypeScript’s generic types or something similar, that functionality does not currently exist in Cadence. Any type that’s accessed from another contract has to be explicitly referenced from a contract import.

How to Generate Run-Time Types

Explain Type<>(), getType(), CompositeType, InterfaceType, RestrictedType, and maybe mention NFTCatalog types

There are multiple ways to create a run-time type in Cadence, so let’s briefly go through each one.

The most straightforward approach is to import the contract that contains the type you want and using that with the Type<>() constructor:

import TopShot from 0x0b2a3299cc857e29
let topShotType: Type = Type<TopShot.NFT>()

Similarly, you can get a type using any value’s built-in getType() method, as demonstrated in the code example in the previous section.

Finally, you can generate a type from a string type identifier using the CompositeType(), InterfaceType(), and RestrictedType() constructors. Each of those constructor functions returns an optional type, as it might evaluate to nil if the input string is invalid.

The CompositeType() function is used to create types of structs and resources, for example:

let topShotType: Type? = CompositeType(“A.0b2a3299cc857e29.TopShot.NFT")

Similarly, the InterfaceType() function is used to create a type representation of an interface:

let receiverType: Type? = InterfaceType("A.1d7e57aa55817448.NonFungibleToken.Receiver")

The RestrictedType() function accepts both a struct or resource type identifier and an array of interface type identifiers to create a type representation of a restricted struct or resource, i.e. a borrowed capability type like &TopShot.Collection{NonFungibleToken.Provider}. You could recreate this type using stings at run-time with the following:

let providerType: Type? = RestrictedType("A.0b2a3299cc857e29.TopShot.Collection", ["A.1d7e57aa55817448.NonFungibleToken.Provider"])

That’s all I have for now on the various types of types in Cadence. Thank you for reading and please reach out with any feedback!

--

--

veganbeef

I write about software engineering, focused on the Flow blockchain