Types, What Are They Good For?

using AbstractTrees

All values have a single concrete concrete type which is a leaf of a single global type tree. All types are "first-class"

julia> typeof(3.14159265)
Float64

julia> typeof(π)
Irrational{:π}

julia> typeof(rand(3))
Vector{Float64}

julia> typeof([1, 1.5, true]) # What happened here?
Vector{Float64}

julia> typeof(1.5 + 1//3 .- [1, 2, 3])
Vector{Float64}

Some languages such as Java have a distinction between certain primitive types like Int64 and classes. While Julia does have primitive and non-primitive types:

julia> isprimitivetype(Int64)
true

julia> isprimitivetype(Vector{String})
false

The user experiences no difference, the distinction is purely for bootstrapping.

Variables are just names, and are untyped

julia> x = 1.0
1.0

julia> typeof(x)
Float64

julia> x = [1,2,3]
[1, 2, 3]

julia> typeof(x)
Vector{Int64}

What about functions?

julia> typeof(sin)
typeof(sin)

julia> typeof((x, y) -> x + y^2) # Why does this look so weird?
Main.__FRANKLIN_1273372.var"#1#2"

Type assertions

The most common place you will see types is constructors:

julia> Int128(3)
3

julia> Vector{Float64}(undef, 2)
[6.9419223265881e-310, 6.94192498419883e-310]

julia> Complex{Float64}(1.0, 3.0)
1.0 + 3.0im

and in type assertions:

julia> 3.0::Float64
3.0

julia> 3.5::Int64
ERROR: TypeError: in typeassert, expected Int64, got a value of type Float64
Stacktrace:

julia> (1.5 + 10)::Float64
11.5

Important - This syntax is central to Julia programming The type assertion syntax ::T is used everywhere from struct definitions to Julia's all-important multiple-dispatch system which we will cover shortly. It is important to get comfortable with the meaning of this syntax

You can read ::Float64 as "is an instance of Float64". A type assertion ensures or states that the left hand side is a subtype of the right hand side. What does that mean?

The Type Tree

You may know about type inheritance from a language like Java or C++, or even Python. In those languages you can create a type (classes in some languages), and then create a second type that inherits the fields and behavior of the parent.

Julia on the other has a tree of abstract types (which contain no structure), and what we call concrete types on the leaves:

AbstractTrees.children(x::Type) = subtypes(x)

# All of the subtypes of the abstract type Number in a tree
print_tree(Number)
Number
├─ MultiplicativeInverse
│  ├─ SignedMultiplicativeInverse
│  └─ UnsignedMultiplicativeInverse
├─ Complex
└─ Real
   ├─ AbstractFloat
   │  ├─ BigFloat
   │  ├─ BFloat16
   │  ├─ Float16
   │  ├─ Float32
   │  └─ Float64
   ├─ AbstractIrrational
   │  └─ Irrational
   ├─ Integer
   │  ├─ Bool
   │  ├─ Signed
   │  │  ├─ BigInt
   │  │  ├─ Int128
   │  │  ├─ Int16
   │  │  ├─ Int32
   │  │  ├─ Int64
   │  │  └─ Int8
   │  └─ Unsigned
   │     ├─ UInt128
   │     ├─ UInt16
   │     ├─ UInt32
   │     ├─ UInt64
   │     └─ UInt8
   └─ Rational

Let's explore this type tree a little bit. At the top we have:

help?> Number
Number

Abstract supertype for all number types.

Since Number is not a leaf on our tree it is an abstract type. A fragment of this type tree would be defined as follows:

abstract type Number end
abstract type Real          <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer       <: Real end
abstract type Signed        <: Integer end
abstract type Unsigned      <: Integer end

abstract type should be fairly self explanatory, but we have a new operator! The <: subtype operator. Number has an implicit supertype here:

supertype(Number)
Any
help?> Any
Any::DataType

Any is the union of all types. It has the defining property isa(x, Any) == true for any x. Any therefore describes the entire universe of possible values. For example Integer is a subset of Any that includes Int, Int8, and other integer types.

We have already seen Any many times without realizing it:

foo(a, b) = 2a + 3b

is equivalent to:

foo(a::Any, b::Any) = 2a + 3b

Immediately you should notice that we might specialize foo on some subtypes of Any.

Structs

We've made do thus far with types defined by Julia, it's time we give it a shot!

One of the simplest types we might create is a Point consisting of two values x and y:

struct Point1
    x
    y
end
julia> p = Point1(19, 97) # construct a Point1
Main.__FRANKLIN_1273372.Point1(19, 97)

julia> p.x # access a field of p
19

julia> p2 = Point1("North", [1, 2, 3, 4])
Main.__FRANKLIN_1273372.Point1("North", [1, 2, 3, 4])

julia> p.y
97

Some features of a struct

julia> p.x = 2
ERROR: setfield!: immutable struct of type Point1 cannot be changed
Stacktrace:
  [1] setproperty!(x::Main.__FRANKLIN_1273372.Point1, f::Symbol, v::Int64)
    @ Base ./Base.jl:53

julia> p.y[1] = 10 # but not necessarily their fields
ERROR: MethodError: no method matching setindex!(::Int64, ::Int64, ::Int64)
The function `setindex!` exists, but no method is defined for this combination of argument types.
Stacktrace:

julia> p
Main.__FRANKLIN_1273372.Point1(19, 97)

julia> 
julia> Point1(1, 2) === Point1(1, 2)
true

julia> 
help?> ===
===(x,y) -> Bool
≡(x,y) -> Bool

Determine whether x and y are identical, in the sense that no program could distinguish them. First the types of x and y are compared. If those are identical, mutable objects are compared by address in memory and immutable objects (such as numbers) are compared by contents at the bit level. This function is sometimes called "egal". It always returns a Bool value.

Examples

julia> a = [1, 2]; b = [1, 2];

julia> a == b
true

julia> a === b
false

julia> a === a
true

Why might Point1 be bad?

Implicitly the fields x and y are of type Any:

struct Point1
    x::Any
    y::Any
end

Important - p.x and p.y retained their type despite the fact that the fields of Point1 are Any. This is a fundamental feature of the language, that values never change type. However we will see that since the compiler will have trouble predicting the value of those fields ahead of time it must be pessimistic

We can instead restrict the types of each field by either concrete or abstract types:

struct Point2
    x::Float64
    y::Number
end
julia> Point2(1.5, 3)
Main.__FRANKLIN_1273372.Point2(1.5, 3)

julia> Point2([1,2,3], 4)
ERROR: MethodError: Cannot `convert` an object of type Vector{Int64} to an object of type Float64
The function `convert` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  convert(::Type{T}, !Matched::T) where T<:Number
   @ Base number.jl:6
  convert(::Type{T}, !Matched::Number) where T<:Number
   @ Base number.jl:7
  convert(::Type{T}, !Matched::T) where T
   @ Base Base.jl:126
  ...

Stacktrace:
  [1] Main.__FRANKLIN_1273372.Point2(x::Vector{Int64}, y::Int64)
    @ Main.__FRANKLIN_1273372 ./string:2

Mutable structs

Mutable types are created in a similar manner:

mutable struct Point3 <: Any
    x::Float64
    y::ComplexF64
end

But they differ in two ways:

  1. We can mutate their fields:
julia> p = Point3(1.0, 10 + 1im)
Main.__FRANKLIN_1273372.Point3(1.0, 10.0 + 1.0im)

julia> p.x = 10
10

julia> p
Main.__FRANKLIN_1273372.Point3(10.0, 10.0 + 1.0im)

julia> p2 = Point3(10, 10 + 1im)
Main.__FRANKLIN_1273372.Point3(10.0, 10.0 + 1.0im)

julia> p == p2 # we can write a new method for == to make this true
false

julia> p === p2 # we cannot make this true
false

julia> 
  1. Since the value named p can change over time it can only be identified by it's address in memory. Hence types declared as mutable struct are typically stored with a stable address in memory.

When should you use a struct or a mutable struct?

Parametric Types

Our first Point struct had fields which could contain any value. That freedom is great, and Julia happily accepts this level of dynamism. But we have another often better solution with parametrization.

We have already seen parametric types before:

mutable struct Vector{T} <: DenseVector{T}
    ref::MemoryRef{T}
    size::Tuple{Int64}
end
mutable struct Matrix{T} <: DenseMatrix{T}
    ref::MemoryRef{T}
    size::Tuple{Int64, Int64}
end
mutable struct Array{T, N} <: DenseArray{T, N}
    ref::MemoryRef{T}
    size::NTuple{N, Int64}
end

Let's take a look at the type tree they are part of:

print_tree(AbstractArray)
AbstractArray
├─ AbstractRange
│  ├─ LinRange
│  ├─ OrdinalRange
│  │  ├─ AbstractUnitRange
│  │  │  ├─ IdentityUnitRange
│  │  │  ├─ OneTo
│  │  │  ├─ Slice
│  │  │  └─ UnitRange
│  │  └─ StepRange
│  └─ StepRangeLen
├─ AbstractSlices
│  └─ Slices
├─ ExceptionStack
├─ LogRange
├─ LogicalIndex
├─ MethodList
├─ ReinterpretArray
├─ ReshapedArray
├─ SCartesianIndices2
├─ WithoutMissingVector
├─ BitArray
├─ CartesianIndices
├─ AbstractRange
│  ├─ LinRange
│  ├─ OrdinalRange
│  │  ├─ AbstractUnitRange
│  │  │  ├─ IdentityUnitRange
│  │  │  ├─ OneTo
│  │  │  ├─ Slice
│  │  │  ├─ StmtRange
│  │  │  └─ UnitRange
│  │  └─ StepRange
│  └─ StepRangeLen
├─ BitArray
├─ ExceptionStack
├─ LinearIndices
├─ LogRange
├─ MethodList
├─ TwoPhaseDefUseMap
├─ TwoPhaseVectorView
├─ DenseArray
│  ├─ Array
│  ├─ CodeUnits
│  ├─ Const
│  ├─ GenericMemory
│  └─ UnsafeView
├─ AbstractTriangular
│  ├─ LowerTriangular
│  ├─ UnitLowerTriangular
│  ├─ UnitUpperTriangular
│  └─ UpperTriangular
├─ Adjoint
├─ Bidiagonal
├─ Diagonal
├─ Hermitian
├─ SymTridiagonal
├─ Symmetric
├─ Transpose
├─ Tridiagonal
├─ UpperHessenberg
├─ LinearIndices
├─ PermutedDimsArray
├─ SubArray
└─ GenericArray

The type tree for AbstractArray is massive, and that's with very few additional packages loaded which may add new subtypes. But it's deceptively small. We know that at least three of the types in this tree are parametric, and each one of those contains a potentially infinite subtree of subtypes.

So let's make our own infinite tree of subtypes:

struct Point{T}
    x::T
    y::T
end

Play around with substituting various types, and let's examine the subtyping relationships which are a little bit subtle!

julia> Point{Complex{Float64}}
Main.__FRANKLIN_1273372.Point{ComplexF64}

julia> Point{AbstractArray}
Main.__FRANKLIN_1273372.Point{AbstractArray}

julia> Point{Complex{Float64}} <: Point
true

julia> Point{AbstractArray} <: Point
true

julia> typeof(rand(10, 10)) <: Point
false

julia> # is equivalent to:

julia> rand(10, 10) isa Point
false

julia> isconcretetype(Point)
false

julia> isconcretetype(Point{AbstractArray})
true

What will happen when I run this code?

Point{Float16} <: Point{BigInt}

Float64 <: Real

Point{Float16} <: Point{Real}

A Point{Float16} has a different representation from a Point{Real} in memory which at a low level must contain two pointers to arbitrary subtypes of Real while Point{Float16} is just 4-bytes

We can overcome this limitation if needed using the subtype operator <: again:

julia> Point{Float16} <: Point{<:Real}
true

Abstract types can be parametric as well, with similar rules:

help?> AbstractArray
AbstractArray{T,N}

Supertype for N-dimensional arrays (or array-like types) with elements of type T. and other types are subtypes of this. See the manual section on the man-interface-array.

See also: , , , .

What's the type of a type?

julia> typeof(Int64)
DataType

julia> typeof(Vector) # 🤔
UnionAll

julia> typeof(Vector{Float64})
DataType

The first and last examples right above were concrete types. But the middle one is a parametric type with no parameter!

Tuples

When you think of a Tuple type you should think of it as analogous to the arguments of a function:

function foo(a, b, c)
    [a, b, c]' .^ 2 * [a, b, c]
end
foo (generic function with 1 method)

Advanced Topics

Unions

UnionAlls

CC BY-SA 4.0 Raye Skye Kimmerer. Last modified: July 14, 2025.
Website built with Franklin.jl and the Julia programming language.