FAQ: I'm confused by the similar looking expressions "1 as UInt8" and UInt8(1), or 10.0 and UFix64(10). When should one be used over the other?

The as operator

The as operator is used to make a static type cast, i.e. it can be used when the type checker can guarantee that the value has the given type.

For example, the number literal 1 is inferred to have the type Int by default.
If however it should be considered an UInt8, the static cast 1 as UInt8 can be performed.
The type checker is able to check that the constant value 1 fits in the type UInt8, which has a range of 0 to 255.

However, consider the following program:

fun intToUInt8(_ value: Int): UInt8 {
    return value as UInt8
}

This program is not valid, because the type checker cannot guarantee that all possible values for variable value (any integer!) will fit in the range of UInt8.

Let us now consider fixed-point numbers. A fixed-point literal like 1.2 is inferred to have the type UFix64.
If however it should be considered a Fix64, the static cast 1.2 as Fix64 can be performed.
The type checker is able to check that the constant value 1.2 fits in the type Fix64.
However, a cast like -1.0 as UFix64 will not type-check, because the type checker is able to determine that -1.0 is out of range for type UFix64, which has a minimum of 0.0.

Similarly, consider the following program which uses resource interfaces and resources:

resource interface NFT {}

resource Kitty: NFT {}

let nft <- create Kitty()

Here, thee variable nft is inferred to have type @Kitty.
In some cases it might make sense to declare the variable with a less specific type, for example to prevent certain functions from being called accidentally.
So in the example, the static cast create Kitty() as @{NFT} can be performed.
The type checker is able to check that the type of create Kitty(), @Kitty is a subtype of @{NFT}, a kitty is definitely always an NFT.
However, the opposite is not true: a static type cast from @{NFT} to @Kitty is not valid, because not all NFTs are kitties.

Type conversion functions

There are cases where it is necessary to convert a value from one type to another.
For example, a function may take some value of type Int and may need to convert it to another integer type, like UInt8, or a fixed-point type like UFix64.
The type checker can not guarantee that all integer values of type Int can be converted to UInt8 or UFix64.

For this purpose case Cadence offers number conversion functions for all number types.

For example, to convert any number to type UInt8, use the type conversion function UInt8.

These type conversion functions accept any number value, i.e. integer or fixed-point value, and attempt to convert the value into the target type.
If the given value is not convertible, the function aborts the program!

For example, the function intToUInt8 above could be written like so:

fun intToUInt8(_ value: Int): UInt8 {
    return UInt8(value)
}

This program is valid and the type checker will accept it: the value passed to the function UInt8 has type Int, which is a number.
However, not all calls to intToUInt8 will succeed, as not all values of type Int can be converted at run-time to type UInt8,
e.g. negative values (the minimum of UInt8 is 0).

Conclusion

Prefer using the static type cast when possible, because it is more efficient, as no conversion needs to be performed when the program is run.
For example, prefer 1 as UInt8 over UInt8(1), as for number literals a static type cast is possible.

(A future version of Cadence could optimize the latter case, the type conversion function call, when it can, but this optimization does currently not exist.)

Use a type conversion function when you want to convert between different types, at run-time.

1 Like