Types

We encountered a few types in the last section, let's take a look in more detail

julia> using About

julia> about(1.0)
Float64 (<: AbstractFloat <: Real <: Number <: Any), occupies 8B.

 0011111111110000000000000000000000000000000000000000000000000000 
 β•¨β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 +    2^0   Γ—                1.000000000000000000                
 = 1.0000000000000000

julia> about(1)
Int64 (<: Signed <: Integer <: Real <: Number <: Any), occupies 8B.

 0000000000000000000000000000000000000000000000000000000000000001
 = +1

julia> about(true)
Bool (<: Integer <: Real <: Number <: Any), occupies 1B.

 00000001 = true

julia> about((1,2, "asdf", "😠"))
Tuple{Int64, Int64, String, String} (<: Any)
 Memory footprint: 32B directly (referencing 56B in total)
 1::Int64  8B 00000000000000000000 … 00000000000000000001 1
 2::Int64  8B 00000000000000000000 … 00000000000000000010 2
 3::String 8B @ 0x00007fd53ccd6798                        "asdf"
 4::String 8B @ 0x00007fd53ccd6818                        "😠"

 β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– 
        8B              8B              *               *        

 * = Pointer (8B)

julia> about([1,2,3,4])
4-element Vector{Int64} (mutable) (<: DenseVector{Int64} <: AbstractVector{Int64} <: Any), occupies 24B directly (referencing 72B in total, holding 32B of data)
  ref::MemoryRef{Int64} 16B Β«structΒ» MemoryRef{Int64}(Ptr … 36a0), [1, 2, 3, 4])
 size::Tuple{Int64}      8B Β«structΒ» (4,)

 β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– 
               16B                      8B       
 Int64 contents exist on the CPU within the ref::MemoryRef from 0x00007fd5476236a0 to 0x00007fd5476236c0.

 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β‹―(Γ— 4)β‹― ───────────────────────────────┐  4 items
 000000010000000000000000         00000000000000000000000000000000    in
 └─0x01β”€β”˜β””β”€0x00β”€β”˜β””β”€0x00β”€β”˜ β‹―(Γ—25)β‹― └─0x00β”€β”˜β””β”€0x00β”€β”˜β””β”€0x00β”€β”˜β””β”€0x00β”€β”˜ 32 bytes

julia> about([1, 1.0, "Howdy"])
3-element Vector{Any} (mutable) (<: DenseVector{Any} <: AbstractVector{Any} <: Any), occupies 24B directly (referencing 93B in total)
  ref::MemoryRef{Any} 16B Β«structΒ» MemoryRef{Any}(Ptr{No … Any[1, 1.0, "Howdy"])
 size::Tuple{Int64}    8B Β«structΒ» (3,)

 β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– 
               16B                      8B       
 Any contents exist on the CPU within the ref::MemoryRef from 0x00007fd53e1f1a00 to 0x00007fd53e1f1a18.

 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β‹―(Γ— 3)β‹― ───────────────────────────────┐  3 pointers
 011000001001001110100011         11010101011111110000000000000000    in
 └─0x60β”€β”˜β””β”€0x93β”€β”˜β””β”€0xa3β”€β”˜ β‹―(Γ—17)β‹― └─0xd5β”€β”˜β””β”€0x7fβ”€β”˜β””β”€0x00β”€β”˜β””β”€0x00β”€β”˜ 24 bytes

julia> about(rand(100))
100-element Vector{Float64} (mutable) (<: DenseVector{Float64} <: AbstractVector{Float64} <: Any), occupies 24B directly (referencing 840B in total, holding 800B of data)
  ref::MemoryRef{Float64} 16B Β«structΒ» MemoryRef{Float64}( … .573433, 0.556942])
 size::Tuple{Int64}        8B Β«structΒ» (100,)

 β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– β– 
               16B                      8B       
 Float64 contents exist on the CPU within the ref::MemoryRef from 0x00007fd53d7ba320 to 0x00007fd53d7ba640.

 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β‹―(Γ—100)β‹― ───────────────────────────────┐ 100 items
 111000000101111101110011          01110111110100101110000100111111     in
 └─0xe0β”€β”˜β””β”€0x5fβ”€β”˜β””β”€0x73β”€β”˜ β‹―(Γ—793)β‹― └─0x77β”€β”˜β””β”€0xd2β”€β”˜β””β”€0xe1β”€β”˜β””β”€0x3fβ”€β”˜ 800 bytes

Exploring the type tree

Julia's type system is organized as a tree, with Any at the root and concrete types at the leaves. We can use AbstractTrees.jl together with subtypes to walk this hierarchy and pretty-print it:

using AbstractTrees, InteractiveUtils
AbstractTrees.children(t::Type) = subtypes(t)

Now print_tree works on any type and renders the subtype hierarchy as a textual diagram. Try it in your REPL:

julia> 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

Tools for exploring types

The tree above gives you the shape. To poke around inside it, Julia has a small kit of reflection tools β€” every one works on the same Number, Real, Integer, Float64, … types you just saw.

The relations on the tree are <: (subtype) and >: (supertype):

julia> Int64 <: Integer
true

julia> Integer <: Number
true

julia> Float64 <: Integer
false

julia> Number >: Float64
true

isa is the value-level version (x isa T ≑ typeof(x) <: T):

julia> 1 isa Integer
true

julia> 1.0 isa Integer
false

julia> 1.0 isa Real
true

julia> (1, 2.0) isa Tuple
true

Walking up and down the tree:

julia> supertype(Int64)
Signed

julia> supertype(Signed)
Integer

julia> supertypes(Int64)               # full chain to Any
(Int64, Signed, Integer, Real, Number, Any)

julia> subtypes(Integer)
Any[Bool, Signed, Unsigned]

julia> subtypes(AbstractFloat)
Any[BigFloat, Core.BFloat16, Float16, Float32, Float64]

Abstract vs. concrete β€” only concrete types can have instances:

julia> isabstracttype(Number)
true

julia> isabstracttype(Int64)
false

julia> isconcretetype(Int64)
true

julia> isconcretetype(Integer)
false

Two more handy ones for reasoning about overlap:

julia> typejoin(Int64, Float64)        # smallest common supertype
Real

julia> typejoin(Int64, String)
Any

julia> typeintersect(Real, Integer)    # largest common subtype
Integer

julia> typeintersect(Float64, Integer) # empty β€” disjoint branches
Union{}

Union{...} lets you talk about "any of these" as a single type:

julia> 1 isa Union{Int, Float64}
true

julia> "hi" isa Union{Int, Float64}
false

julia> Union{Int, Float64} <: Real     # both branches are Real
true

Field introspection β€” handy once your own structs get involved (the Complex type from the Number tree is a good live example):

julia> fieldnames(Complex{Float64})
(:re, :im)

julia> fieldtypes(Complex{Float64})
(Float64, Float64)

julia> fieldtype(Complex{Float64}, :re)
Float64

And to ask "what methods do I have that touch this type?":

julia> methodswith(Unsigned)
Method[rem(x::Union{Int128, Int16, Int32, Int64, Int8}, y::Unsigned) @ Base int.jl:232, rem(x::Unsigned, y::Union{Int128, Int16, Int32, Int64, Int8}) @ Base int.jl:233, rem(x::Unsigned, ::Type{Signed}) @ Base int.jl:626, <<(x::Integer, c::Unsigned) @ Base operators.jl:712, >>>(x::Integer, c::Unsigned) @ Base operators.jl:793, ^(x::BigFloat, y::Unsigned) @ Base.MPFR mpfr.jl:816, abs(x::Unsigned) @ Base int.jl:187, cld(x::Signed, y::Unsigned) @ Base div.jl:350, cld(x::Unsigned, y::Signed) @ Base div.jl:351, div(x::Signed, y::Unsigned, ::RoundingMode{:Up}) @ Base div.jl:304, div(x::Signed, y::Unsigned, ::RoundingMode{:Down}) @ Base div.jl:295, div(x::Unsigned, y::Signed, ::RoundingMode{:Up}) @ Base div.jl:308, div(x::Unsigned, y::Signed, ::RoundingMode{:Down}) @ Base div.jl:299, div(x::T, y::T, ::RoundingMode{:Down}) where T<:Unsigned @ Base div.jl:368, div(x::T, y::T, ::RoundingMode{:Up}) where T<:Unsigned @ Base div.jl:375, div(x::Unsigned, y::Union{Int128, Int16, Int32, Int64, Int8}) @ Base int.jl:230, div(x::Union{Int128, Int16, Int32, Int64, Int8}, y::Unsigned) @ Base int.jl:229, divrem(x::Unsigned, y::Union{Int128, Int16, Int32, Int64, Int8}) @ Base int.jl:240, divrem(x::Union{Int128, Int16, Int32, Int64, Int8}, y::Unsigned) @ Base int.jl:235, fld(x::Signed, y::Unsigned) @ Base div.jl:348, fld(x::Unsigned, y::Signed) @ Base div.jl:349, gcd(a::Signed, b::Unsigned) @ Base intfuncs.jl:150, gcd(a::Unsigned, b::Signed) @ Base intfuncs.jl:149, gcdx(a::Signed, b::Unsigned) @ Base intfuncs.jl:257, gcdx(a::Unsigned, b::Signed) @ Base intfuncs.jl:263, isvalid(::Type{<:AbstractChar}, c::Unsigned) @ Base.Unicode strings/unicode.jl:59, lcm(a::Signed, b::Unsigned) @ Base intfuncs.jl:152, lcm(a::Unsigned, b::Signed) @ Base intfuncs.jl:151, mod(x::T, y::T) where T<:Unsigned @ Base int.jl:297, mod(x::Union{Int128, Int16, Int32, Int64, Int8}, y::Unsigned) @ Base int.jl:289, mod(x::Unsigned, y::Signed) @ Base int.jl:293, print(io::IO, n::Unsigned) @ Base show.jl:1287, show(io::IO, n::Unsigned) @ Base show.jl:1286, sign(x::Unsigned) @ Base number.jl:163, signbit(x::Unsigned) @ Base int.jl:140, widemul(x::Unsigned, y::Signed) @ Base int.jl:841, widemul(x::Signed, y::Unsigned) @ Base int.jl:840]

Advanced: Base.isbitstype(T) tells you whether T is plain old data (stack-allocatable, no pointers) β€” this is the property that lets values like Int64 and Float64 sit inline inside arrays without any indirection.

julia> isbits(1), isbits(1.0), isbits("hi")
(true, true, false)

Defining your own types

Immutable structs

struct (without mutable) is the default β€” once constructed, fields can't be rebound:

julia> struct Point
    x::Float64
    y::Float64
end

julia> p = Point(1.0, 2.0)
Main.__FRANKLIN_1181134.Point(1.0, 2.0)

julia> p.x
1.0

julia> typeof(p)
Main.__FRANKLIN_1181134.Point

Trying to mutate a field is an error:

julia> try
    p.x = 5.0
catch e
    e
end
ErrorException("setfield!: immutable struct of type Point cannot be changed")

Warning: "Immutable" means the fields can't be rebound. If a field happens to hold a mutable value (like a Vector), the contents of that value are still mutable β€” only the binding is frozen.

Mutable structs

Prepend mutable if you need to reassign fields after construction:

julia> mutable struct Counter
    n::Int
end

julia> c = Counter(0)
Main.__FRANKLIN_1181134.Counter(0)

julia> c.n += 1
1

julia> c.n += 1
2

julia> c
Main.__FRANKLIN_1181134.Counter(2)

Mutables are heap-allocated and compared by identity (===) by default, where immutables are compared by value:

julia> Point(1.0, 2.0) == Point(1.0, 2.0)
true

julia> Counter(0) == Counter(0)
false

Parametric structs

A type parameter lets one struct cover many element types β€” and lets the compiler specialise:

julia> struct Pair2{T}
    x::T
    y::T
end

julia> Pair2(1, 2)             # Pair2{Int}
Main.__FRANKLIN_1181134.Pair2{Int64}(1, 2)

julia> Pair2(1.0, 2.0)         # Pair2{Float64}
Main.__FRANKLIN_1181134.Pair2{Float64}(1.0, 2.0)

julia> typeof(Pair2(1, 2))
Main.__FRANKLIN_1181134.Pair2{Int64}

You can constrain the parameter with <::

julia> struct NumPair{T<:Number}
    x::T
    y::T
end

julia> NumPair(1, 2)
Main.__FRANKLIN_1181134.NumPair{Int64}(1, 2)

julia> try
    NumPair("a", "b")
catch e
    e
end
MethodError(Main.__FRANKLIN_1181134.NumPair, ("a", "b"), 0x0000000000009918)

Abstract types and subtyping

abstract type declares a node in the type tree β€” no fields, no constructor, just a label other types can sit under:

julia> abstract type Animal end

julia> struct Dog <: Animal
    name::String
end

julia> struct Cat <: Animal
    name::String
end

julia> Dog("Rex") isa Animal, Cat("Mia") isa Animal
(true, true)

Methods written against the abstract type apply to every concrete subtype:

julia> speak(a::Animal) = "$(a.name) makes a sound"
Main.__FRANKLIN_1181134.var"#speak"()

julia> speak(d::Dog)    = "$(d.name) says woof"
Main.__FRANKLIN_1181134.var"#speak"()

julia> speak(Dog("Rex"))
Rex says woof

julia> speak(Cat("Mia"))
Mia makes a sound

Singleton types (no fields)

A struct with no fields is its own type β€” useful as a dispatch tag (this is exactly what the rock-paper-scissors example below uses):

julia> struct Marker end

julia> Marker() === Marker()
true

Constructors

By default, every struct gets one outer constructor matching its field list. You can add more outer constructors to provide defaults or conversions:

julia> struct Celsius
    value::Float64
end

julia> # Outer constructor: build a Celsius from a Fahrenheit number

julia> Celsius(Β°f::Int) = Celsius((Β°f - 32) * 5 / 9)
Main.__FRANKLIN_1181134.Celsius

julia> Celsius(0)                # 0 Β°F as Β°C
Main.__FRANKLIN_1181134.Celsius(-17.77777777777778)

julia> Celsius(100.0)            # already Β°C
Main.__FRANKLIN_1181134.Celsius(100.0)

An inner constructor lives inside the struct block. It's the only way to enforce invariants β€” you call new(...) instead of recursing into the type, and the default constructor is suppressed once you write one:

julia> struct PositiveInt
    n::Int
    function PositiveInt(n::Int)
        n > 0 || throw(ArgumentError("expected positive, got $n"))
        new(n)
    end
end

julia> PositiveInt(5)
Main.__FRANKLIN_1181134.PositiveInt(5)

julia> try
    PositiveInt(-1)
catch e
    e
end
ArgumentError("expected positive, got -1")

Advanced: Parametric inner constructors use new{T}(...), and you'll often write both a "constrained" inner ctor (enforcing the invariant) and a thin outer ctor (inferring the parameter from the arguments).

Visualising dispatch

A textual tree shows the type hierarchy, but it doesn't show which method Julia would actually call for a given combination of argument types. For that we can use DispatchDisplay.jl, which renders a generic function as a grid coloured by the method dispatch would select for each cell.

Following MosΓ¨ Giordano's classic post, let's encode rock-paper-scissors as multiple dispatch. play dispatches on the types Type{Rock}, Type{Paper}, … β€” each explicit win-rule is its own method:

julia> abstract type Shape end

julia> struct Rock     <: Shape end

julia> struct Paper    <: Shape end

julia> struct Scissors <: Shape end
julia> play(::Type{Paper}, ::Type{Rock})     = "Paper wins"
Main.__FRANKLIN_1181134.var"#play"()

julia> play(::Type{Paper}, ::Type{Scissors}) = "Scissors wins"
Main.__FRANKLIN_1181134.var"#play"()

julia> play(::Type{Rock},  ::Type{Scissors}) = "Rock wins"
Main.__FRANKLIN_1181134.var"#play"()

julia> play(::Type{T}, ::Type{T}) where {T<:Shape} = "Tie, try again"
Main.__FRANKLIN_1181134.var"#play"()

julia> play(a::Type{<:Shape}, b::Type{<:Shape}) = play(b, a)   # commutativity fallback
Main.__FRANKLIN_1181134.var"#play"()

A couple of spot-checks at the REPL:

julia> play(Type{Paper}, Type{Rock})
ERROR: MethodError: no method matching play(::Type{Type{Main.__FRANKLIN_1181134.Paper}}, ::Type{Type{Main.__FRANKLIN_1181134.Rock}})
The function `play` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  play(!Matched::Type{Main.__FRANKLIN_1181134.Rock}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
   @ [Franklin]:1
  play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
   @ [Franklin]:1
  play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Rock})
   @ [Franklin]:1
  ...

Stacktrace:

julia> play(Type{Scissors}, Type{Paper})
ERROR: MethodError: no method matching play(::Type{Type{Main.__FRANKLIN_1181134.Scissors}}, ::Type{Type{Main.__FRANKLIN_1181134.Paper}})
The function `play` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  play(!Matched::Type{Main.__FRANKLIN_1181134.Rock}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
   @ [Franklin]:1
  play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
   @ [Franklin]:1
  play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Rock})
   @ [Franklin]:1
  ...

Stacktrace:

julia> play(Type{Rock}, Type{Rock})
ERROR: MethodError: no method matching play(::Type{Type{Main.__FRANKLIN_1181134.Rock}}, ::Type{Type{Main.__FRANKLIN_1181134.Rock}})
The function `play` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  play(!Matched::Type{Main.__FRANKLIN_1181134.Rock}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
   @ [Franklin]:1
  play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Scissors})
   @ [Franklin]:1
  play(!Matched::Type{Main.__FRANKLIN_1181134.Paper}, !Matched::Type{Main.__FRANKLIN_1181134.Rock})
   @ [Franklin]:1
  ...

Stacktrace:

Now, in your own REPL, load an interactive Makie backend together with DispatchDisplay and render the 3Γ—3 dispatch grid (the lines below are copy-paste-only β€” they need a live OpenGL window so the static site doesn't run them):

using GLMakie            # interactive window β€” best for hovering / 3D
using DispatchDisplay

shapes3 = [Type{Rock}, Type{Paper}, Type{Scissors}]
d3 = dispatchdisplay(play, shapes3, shapes3)

The grid makes the whole game visible at a glance:

Extending the game

Add a Well that beats Rock and Scissors but loses to Paper:

julia> struct Well <: Shape end

julia> play(::Type{Well}, ::Type{Rock})     = "Well wins"
Main.__FRANKLIN_1181134.play

julia> play(::Type{Well}, ::Type{Scissors}) = "Well wins"
Main.__FRANKLIN_1181134.play

julia> play(::Type{Well}, ::Type{Paper})    = "Paper wins"
Main.__FRANKLIN_1181134.play

Then re-render with a 4-way axis (again, copy into your REPL β€” needs GLMakie):

shapes4 = [Type{Rock}, Type{Paper}, Type{Scissors}, Type{Well}]
d4 = dispatchdisplay(play, shapes4, shapes4)

The new row/column is mostly Well-wins, with one Paper-wins cell β€” and the commutativity fallback fills in the lower triangle for free.

CC BY-SA 4.0 Raye Kimmerer. Last modified: June 22, 2026.
Website built with Franklin.jl and the Julia programming language.