Parametric Types & Generics

A type parameter is a placeholder that keeps a type concrete. Vector{T} is not one type but a family: Vector{Int}, Vector{String}, Vector{Vector{Float64}} are all different types, each with its own optimal layout. The parameter is filled in by the compiler, so generic code runs like specialised code.

You have already met parameters in passing: Vector{Int} in collections, TypedBox{T} in composite types, where {T <: Number} in the container ladder. This lesson turns that familiarity into fluency. You will write parametric structs, constrain parameters with where clauses, read abstract parametric types such as AbstractVector{T} correctly, and — most importantly — learn the invariance rule that explains why Vector{Int} is not a Vector{Real}. The lesson closes with the design question every package author faces: when to parameterise, and when a parameter is one too many.

Why Parametric Types

Generic programming begins with a question: how do you write one definition that works for every element type without paying for the generality? Julia answers with type parameters, and the first step is to see what the alternative costs.

The Cost of Loose Types

Without a parameter, a container has to promise only Any. The compiler then cannot know what a field or element contains, so it stores pointers to boxed values and inserts a dynamic lookup on every access. The code is correct, and several times slower than the same code over a concrete type.

# A struct that works for anything — and loses the type information
struct AnyStack
    items::Vector{Any}
end

push_any!(s::AnyStack, x) = push!(s.items, x)

s = AnyStack(Any[])
push_any!(s, 1)
push_any!(s, 2.5)

typeof(s)                  # AnyStack — no element type anywhere in the type

# A parameter keeps the element type in the type itself
struct Stack{T}
    items::Vector{T}
end

push_any!(s::Stack, x) = push!(s.items, x)

si = Stack(Int[])          # Stack{Int64}
sf = Stack(Float64[])      # Stack{Float64}

typeof(si)                 # Stack{Int64}
typeof(sf)                 # Stack{Float64}

The difference is not cosmetic: Stack{Int64} can store its elements inline, and every method written for Stack is compiled with T known. Type stability — the property that a function's return type is predictable from its argument types — follows from exactly this habit.

A First Type Parameter

Parameters are written in curly braces after the type name, and they may be used in field declarations, constructor signatures, and method bodies. A constructor that receives a container can infer the parameter for you.

struct Pair2{A, B}
    first::A
    second::B
end

# Inference from the arguments — no explicit braces needed
p = Pair2(1, "one")        # Pair2{Int64, String}
typeof(p)                  # Pair2{Int64, String}
p.first                    # 1
p.second                   # "one"

# Explicit instantiation when the inference is not what you want
q = Pair2{Float64, Float64}(1, 2)   # Pair2{Float64, Float64}
q.first                             # 1.0

# The parameters are part of the type, so different parameters are different types
Pair2(1, 2) === Pair2{Int64, Int64}(1, 2)   # true — same concrete type
Pair2(1, 2) isa Pair2                        # true — Pair2 alone is the UnionAll family
Pair2(1, 2) isa Pair2{Int64, Int64}          # true — the concrete instantiation

# The empty-braces family form is how you talk about all instantiations
stack_types = [Stack(Int[]), Stack(Float64[])]
all(s -> s isa Stack, stack_types)            # true

Write Pair2 in a type annotation and you accept every instantiation; write Pair2{Int, String} and you accept exactly one. Both are useful, and choosing deliberately is the whole skill.

Concrete Instantiations

A parametric type is abstract until every parameter is supplied — that is why Stack cannot store anything usefully and Stack{Int64} can. The family form is called a UnionAll type, and it exists to describe sets of concrete types.

Stack                 # Stack — a UnionAll: "Stack of any T"
Stack{Int}            # Stack{Int64} — concrete, usable for storage
Vector                # Vector — any element type, any length is not involved
Vector{Int}           # Vector{Int64} — concrete

# isconcretetype answers the question directly
isconcretetype(Stack)          # false
isconcretetype(Stack{Int})     # true
isconcretetype(Vector{Any})    # true — Any is a parameter value like any other

# typeof always reports the concrete instantiation
typeof(Stack(Int[]))           # Stack{Int64}
typeof([1, 2, 3])              # Vector{Int64}

# eltype is not automatic — declare it when your type behaves as a container
Base.eltype(::Type{Stack{T}}) where {T} = T
eltype([1, 2, 3])              # Int64 — from Base's AbstractArray methods
eltype(Stack(Int[]))           # Int64 — from the method above

# Instantiation is a type-level operation, available at runtime too
T = Int
Stack{T} isa Type              # true

Two habits come out of this: annotate Stack{T} in method signatures when you need the element type, and annotate plain Stack only when the method genuinely does not care. Both are dispatch decisions, and both are visible in @which.

Type Parameters

A definition may take any number of parameters, of any kinds Julia supports: types, values, and even other parametric types. Understanding what each kind buys you is what makes a design readable.

Several Parameters

A matrix-shaped type usually carries an element type and a dimension, or an element type plus a storage choice. Parameters compose freely, and the conventional naming is T for an element type, N for a dimension, and S or V for a secondary type.

struct Grid{T, N}
    data::Array{T, N}
end

Grid(zeros(2, 3))              # Grid{Float64, 2}
Grid([1, 2, 3])                # Grid{Int64, 1}
Grid(zeros(Float32, 2, 3))     # Grid{Float32, 2}

# Reading parameters back out of a type
function describe(g::Grid{T, N}) where {T, N}
    "$(N)-dimensional grid of $T"
end

describe(Grid(zeros(3)))       # "1-dimensional grid of Float64"
describe(Grid(zeros(2, 3)))    # "2-dimensional grid of Float64"

# Parameters may also be nested types
struct Matrix2{T}
    rows::Vector{Vector{T}}    # a vector of vectors
end

Matrix2([[1, 2], [3]])         # Matrix2{Int64}

# And a parameter may have a default, declared in the type body
struct Config2{T = Float64}
    value::T
end

Config2(1.0)                   # Config2{Float64}
Config2{Float64}(1)            # Config2{Float64}, value converted

Default parameters are a useful convenience in packages: users get the common instantiation without spelling it out, while code that needs a different one can still ask for it.

Value Parameters

Parameters are not restricted to types. An integer or symbol parameter turns a value into part of the type, which lets the compiler specialise on it — the standard library does this with Vector{3}-style array sizes and with Val.

struct SizedVector{N, T}
    data::Vector{T}

    function SizedVector{N, T}(data) where {N, T}
        length(data) == N || throw(ArgumentError("expected $N elements"))
        new{N, T}(collect(data))
    end
end

SizedVector{3, Int}([1, 2, 3])    # SizedVector{3, Int64}
SizedVector{2, Int}([1, 2])      # SizedVector{2, Int64}
SizedVector{3, Int}([1, 2])      # ERROR: ArgumentError: expected 3 elements

# The size is part of the type, so the compiler knows it
struct Vector3{T}
    x::T; y::T; z::T
end

Vector3(1.0, 2.0, 3.0)         # Vector3{Float64}

# Val carries a value at the type level, for dispatch
first_n(v::Val{N}, xs) where {N} = xs[1:N]

first_n(Val(2), [10, 20, 30, 40])   # [10, 20]
first_n(Val(3), [10, 20, 30, 40])   # [10, 20, 30]

# Val is how you dispatch on a value without paying for it at runtime
layout(::Val{:row}) = :row_major
layout(::Val{:col}) = :column_major

layout(Val(:row))              # :row_major

Value parameters and Val are the standard way to get compile-time specialisation for sizes, orientations, and flags. The cost is compilation time: every distinct value creates a new method instance.

Parameters Inside Field Types

Because a parameter is a real type, it can appear inside a field's type — as the element type of a vector, the key type of a dictionary, or the argument of another parametric type. This is how a struct advertises exactly what it can hold.

struct Inventory{T}
    counts::Dict{String, T}        # T appears nested inside the field type
    names::Vector{String}
end

Inventory(Dict("bolts" => 12))    # Inventory{Int64}
Inventory(Dict("bolts" => 12.0))  # Inventory{Float64}

# Constraints can be attached at the field level too
struct Positive{T <: Real}
    value::T
end

Positive(3)                        # Positive{Int64}
Positive(3.5)                      # Positive{Float64}
Positive("three")                  # ERROR: MethodError — String is not Real

# A parameter used in two fields forces them to agree
struct Ratio{T <: Real}
    num::T
    den::T                         # same T: fields must match
end

Ratio(1, 2)                        # Ratio{Int64}
Ratio(1, 2.0)                      # ERROR: MethodError — mixed types need a convert method
Ratio(Float64(1), 2)               # Ratio{Float64} — make the conversion explicit

That last error is a feature: forcing both fields to share T means code downstream never has to wonder whether the numerator and denominator have the same type. Add a convenience constructor when callers should be able to mix types.

Left: Int is a subtype of Real, but Vector{Int} is not a subtype of Vector{Real}, because element parameters are invariant. Right: the UnionAll type Vector{T} where T is a subtype of Real instantiates to Vector{Int} when called with integers.
Parameters instantiate a family; they do not inherit from one another.

Constraints and where Clauses

A parameter with no constraint accepts everything, including types that make no sense for your algorithm. Constraints narrow the family, and Julia expresses them with <: in the definition or with a where clause in a method.

Subtype Constraints

A constraint written as T <: Number means "any concrete type that is a subtype of Number". It is not a conversion and not a check at run time: the constraint decides which instantiations of your type can exist at all.

struct Average{T <: Real}
    values::Vector{T}
end

Average([1, 2, 3])                 # Average{Int64}
Average([1.0, 2.0])                # Average{Float64}
Average(["a", "b"])                # ERROR: MethodError — String is not Real

# The constraint is visible in the type parameters
Average([1, 2, 3]) isa Average     # true — the family
Average([1, 2, 3]) isa Average{Int}    # true
Average([1, 2, 3]) isa Average{Real}   # false — Int is not Real itself

# Functions can be constrained the same way
mean_of(a::Average{T}) where {T <: Real} = Float64(sum(a.values) / length(a.values))

mean_of(Average([1, 2, 3]))        # 2.0

# A looser constraint accepts a wider family
struct Wrapper{T}
    value::T
end

unwrap(w::Wrapper{T}) where {T <: AbstractString} = lowercase(w.value)
unwrap(Wrapper("TEXT"))            # "text"
unwrap(Wrapper(3))                 # ERROR: MethodError — Int has no unwrap method

Notice the difference between T <: Real and Real as a field type. The constraint keeps T concrete for each instantiation; the abstract field type erases it. Constraints preserve speed, abstract fields do not.

where Syntax

where introduces and constrains parameters in method signatures. It may appear after the parameter list, and several clauses chain from left to right, with later parameters allowed to depend on earlier ones.

# One parameter, unconstrained
identity2(x::T) where {T} = x

# One parameter, constrained
double(x::T) where {T <: Number} = 2x

# Several parameters, one depending on the other
struct Container{T, S <: AbstractVector{T}}
    data::S
end

Container([1, 2, 3])               # Container{Int64, Vector{Int64}}
Container(1:3)                     # Container{Int64, UnitRange{Int64}}

# where clauses in a function mirror the type definition
size_of(c::Container{T, S}) where {T, S} = length(c.data)

# Constraints may be unions
show_type(x::Union{Int, Float64, Nothing}) = typeof(x)
show_type(1)                       # Int64
show_type(nothing)                 # Nothing
show_type("a")                     # ERROR: MethodError

# where also documents which parameter a method needs
swap_pair(p::Pair2{A, B}) where {A, B} = Pair2(p.second, p.first)
swap_pair(Pair2(1, "one"))         # Pair2{String, Int64}("one", 1)

Read a signature as a promise: where {T <: Number} promises your body that T supports everything the Number interface declares, and promises callers that unsupported types will be rejected before the body runs.

Constraints Inside Type Definitions

Constraints can also be written inline in the braces of a type annotation — Vector{<:Real} — which is shorthand for a where clause and reads naturally at call sites and in field declarations.

# Vector{<:Real} is shorthand for Vector{T} where {T <: Real}
takes_any_real?(v::Vector{<:Real}) = true
takes_any_real?([1, 2, 3])          # true
takes_any_real?([1.0, 2.0])         # true

takes_any_real?(["a"])              # ERROR: MethodError — Vector{String} does not match

# Inside a struct field, the shorthand constrains what may be stored
struct Series
    values::Vector{<:Real}
end

Series([1, 2, 3])                   # accepted — stored as Vector{Int64}
Series(["a"])                       # ERROR: MethodError

# The two spellings are interchangeable in a method signature
f1(v::Vector{T}) where {T <: Real} = "long form"
f2(v::Vector{<:Real}) = "short form"

# Both accept the same arguments; matching both means a method is more specific
g(v::Vector{<:Real}) = "real vector"
g(v::Vector) = "any vector"
g([1, 2])                           # "real vector"
g(["a"])                            # "any vector"

Prefer the shorthand inside annotations and the where form when the body needs to use the parameter by name. Above all, constrain when the body requires a method the type must provide — that constraint is documentation the compiler enforces.

Abstract Parametric Types

The standard library uses parameters in its abstract types too: AbstractVector{T}, AbstractDict{K, V}, and the numeric tower. Reading those definitions correctly — and knowing what subtyping does and does not give you — is what lets your own types plug into existing algorithms.

Abstract Types With Parameters

An abstract type may declare parameters, and any concrete subtype must supply them. That is how Vector{Int}, a range, and a custom array all agree on what their elements are, so algorithms written for AbstractArray{T, N} work on all of them.

# A small slice of the AbstractArray family in Base
# abstract type AbstractArray{T, N} end
# abstract type AbstractVector{T} <: AbstractArray{T, 1} end

# Concrete types fill the parameters in
Vector{Int} <: AbstractVector{Int}          # true
Vector{Int} <: AbstractArray{Int, 1}        # true
Vector{Int} <: AbstractVector               # true — any element type

# Your own array subtype joins every existing algorithm
struct DoublingArray{T, N} <: AbstractArray{T, N}
    data::Array{T, N}
end

Base.size(a::DoublingArray) = size(a.data)
Base.getindex(a::DoublingArray, i::Int...) = 2 * a.data[i...]

d = DoublingArray([1, 2, 3])
d[2]                            # 4 — getindex is what we defined
sum(d)                          # 12 — sum works through size and getindex
d isa AbstractVector{Int}       # true
eltype(d)                       # Int64 — inherited from the parameter

That is the payoff of abstract parametric types: declare <: AbstractArray{T, N}, implement two small methods, and the type inherits iteration, broadcasting, sum, map, and dozens of other functions you did not write.

Subtyping Rules and Invariance

Subtyping is not symmetric across positions: a subtype relationship in one place does not carry into a type parameter. The rule is invariance, and it exists to keep types honest about what they can hold.

QuestionAnswerWhy
Int <: RealtrueConcrete subtype of an abstract type
Vector{Int} <: Vector{Real}falseElement parameters are invariant
Vector{Int} <: AbstractVector{Real}falseInvariance applies to abstract parents too
Vector{Int} <: AbstractVector{<:Real}trueThe <: asks for "some real subtype"
Vector{Int} <: AbstractVectortrueDropping a parameter accepts the whole family
Tuple{Int, Int} <: Tuple{Real, Real}trueTuples are covariant — a documented exception
f(v::Vector{Real}) = "a real vector"
g(v::Vector{<:Real}) = "some vector of reals"

f([1.0, 2.0])               # "a real vector"
f([1, 2])                   # ERROR: MethodError — Vector{Int} is not Vector{Real}
g([1, 2])                   # "some vector of reals" — the <: form accepts Int
g([1.0, 2.0])               # "some vector of reals"

# Tuples are the exception, because every field type is known exactly
one_way(t::Tuple{Real, Real}) = "two reals"
one_way((1, 2))             # "two reals" — Tuple{Int, Int} <: Tuple{Real, Real}

# The practical rule: accept containers with <:, never with a fixed parameter
h(v::AbstractVector) = "any vector"
h([1, 2, 3])                # "any vector"
h(1:3)                      # "any vector" — a range is an AbstractVector too

This is why library signatures read v::AbstractVector or v::AbstractVector{<:Real} rather than v::Vector{Real}. The narrower spelling rejects arguments that ought to work, and the rejection arrives as a confusing MethodError at the call site.

Reading Type Relationships

Four functions answer every practical question about a parametric type: supertype, subtypes, the subtype operator itself, and unwrap_unionall for peeking at the family behind a concrete instantiation.

T = Vector{Int}

supertype(T)                    # AbstractVector{Int64} — one step up the tree
T <: AbstractVector             # true  — any element type
T <: AbstractArray              # true  — any dimension
T <: AbstractVector{Float64}    # false — invariance again

# The parameters of a type, with or without their variables
Base.unwrap_unionall(T).parameters       # svec(Int64, 1) — element type and dimension
Base.unwrap_unionall(Vector).parameters  # contains a TypeVar for the element

# Test a type before trying to instantiate it
isconcretetype(Vector{Int})     # true
isconcretetype(Vector)          # false

# Walking up a small hierarchy is how trait functions are written
abstract type Animal2 end
struct Dog2 <: Animal2 end
supertype(Dog2)                 # Animal2
subtypes(Animal2)               # [Dog2]

When a signature does not match, print typeof(argument) and compare it against the annotation piece by piece. Nine times out of ten the mismatch is a missing or extra parameter, not a genuinely unsupported value.

Generics in Practice

Parameters are a tool for writing code once and running it fast everywhere. The craft is in knowing where to introduce one, how to keep inference working, and when to stop.

Writing Type-Stable Generic Functions

Type stability means the compiler can predict a function's return type from the types of its arguments. Generic code is type-stable when every branch returns the same type and no untyped value leaks into a computation.

# Stable: both branches return T
function safe_div(a::T, b::T) where {T <: Real}
    b == 0 ? zero(T) : a / b
end

@code_warntype safe_div(1.0, 2.0)      # Float64 — predictable

# Unstable: the branches produce unrelated types
function pick(flag::Bool)
    flag ? 1 : "none"                  # Int64 or String
end

@code_warntype pick(true)              # Union{Int64, String} — callers must handle both

# Unstable and avoidable: an accumulator seeded with a literal
function total(v)
    t = 0                              # Int64 initially
    for x in v
        t += x                         # with Float64 elements t must change type
    end
    t
end

@code_warntype total([1.5, 2.5])       # t::Union{Int64, Float64} — a boxed variable

# Stable version: seed the accumulator from the element type
function total2(v::AbstractVector{T}) where {T <: Number}
    t = zero(T)                        # T is known, so t never changes type
    for x in v
        t += x
    end
    t
end

@code_warntype total2([1.5, 2.5])      # Float64 — predictable

The pattern is always the same: seed an accumulator with zero(T), one(T), or similar, never with a bare literal. A generic function is only as fast as its least predictable variable.

Constructor Inference

Well-designed parametric types are pleasant to construct: the parameters are inferred from the arguments, and braces are needed only when the inference would differ from what you want. Add convenience constructors to keep the common cases short.

struct Dataset{T}
    features::Matrix{T}
    labels::Vector{Int}
end

# Inference from the matrix element type, with a shape check
function Dataset(features::Matrix{T}, labels::Vector{Int}) where {T}
    size(features, 1) == length(labels) || throw(DimensionMismatch("rows != labels"))
    Dataset{T}(features, labels)
end

Dataset([1.0 2.0; 3.0 4.0], [1, 2])       # Dataset{Float64}
Dataset([1 2; 3 4], [1, 2])               # Dataset{Int64}
Dataset([1.0 2.0], [1, 2])                # ERROR: DimensionMismatch

# A convenience form for a common shape
Dataset(labels::Vector{Int}) = Dataset(zeros(Float64, length(labels), 0), labels)
Dataset([1, 2, 3])                        # Dataset{Float64} with zero feature columns

# Reading the parameter back out where an algorithm needs it
Base.eltype(::Type{Dataset{T}}) where {T} = T
eltype(Dataset([1.0 2.0; 3.0 4.0], [1, 2]))   # Float64

The convenience constructor is not a different type: it delegates to the same parametric one. Validation stays in a single place while the call site stays short — the pattern you already saw with @kwdef in composite types.

Aliases for Readable Signatures

A const alias gives a long parametric type a readable name. It is not a new type — the compiler treats it as the same one — so it costs nothing and can be used in annotations and in dispatch.

# A readable alias for a long type
const FeatureMatrix = Matrix{Float64}
const Labels = Vector{Int}

struct Dataset2
    features::FeatureMatrix
    labels::Labels
end

FeatureMatrix === Matrix{Float64}      # true — an alias, not a new type

# An alias for an open family, useful in signatures
const RealVector = AbstractVector{<:Real}

[1, 2, 3] isa RealVector               # true
[1.0, 2.0] isa RealVector              # true
["a"] isa RealVector                   # false

# Aliases participate in dispatch exactly like the type they name
sum_real(v::RealVector) = sum(v)
sum_real([1, 2, 3])                    # 6

# An alias may also name a partial instantiation
const FloatDataset = Dataset{Float64}
Dataset([1.0 2.0; 3.0 4.0], [1, 2]) isa FloatDataset   # true

Use aliases to make signatures readable and to name an important distinction, such as "features must be floating point". Do not use one to hide a parameter you actually need inside the body — that trade makes the code shorter and harder to follow.

Common Pitfalls

Three mistakes account for most surprise in generic code: a type variable that quietly degrades to Any, a signature that contradicts invariance, and a design with one parameter too many.

When a Parameter Silently Becomes Any

A type variable that appears in no field is not constrained by anything, so Julia may bind it to Any. The result is a container that loses its element type exactly where you thought you had protected it.

# T appears only in the type name, never in a field: it constrains nothing
struct Loose{T}
    values::Vector
end

Loose{Int}([1, 2, 3])                   # Loose{Int64} — but look at the field
typeof(Loose{Int}([1, 2, 3]).values)    # Vector{Any}

# The field must mention the parameter for it to matter
struct Tight{T}
    values::Vector{T}                   # now T drives the storage
end

Tight{Int}([1, 2, 3])                   # Tight{Int64}
typeof(Tight{Int}([1, 2, 3]).values)    # Vector{Int64}

# An empty container literal is the other route to Any
Tight{Int}([])                          # ERROR: MethodError — Vector{Any} is not Vector{Int}
Tight{Int}(Int[])                       # Tight{Int64} — annotate the empty vector

# The same trap in functions: a parameter that does not tie arguments together
uses(x::T, y::T) where {T} = (x, y)     # T ties both arguments to one type
@code_warntype uses(1, 2)               # (Int64, Int64) — both inferred
uses(1, "a")                            # ERROR: MethodError — no single T fits

Check for the trap with fieldtypes: when a declared parameter appears in no field type, the parameter is decorative and the field it was meant to protect is Any.

MethodError from Invariance

A signature written with a concrete parameter rejects arguments that are semantically fine. This is the most common generic bug in the wild, and the fix is always the same: loosen the annotation.

SymptomCauseRewrite as
f(v::Vector{Real}) rejects [1, 2]A concrete parameter is invariantf(v::Vector{<:Real})
g(m::Matrix{Int}) rejects a viewA concrete container typeg(m::AbstractMatrix{<:Integer})
h(d::Dict{String, Int}) rejects Dict{String, Int32}Invariance in both parametersh(d::AbstractDict{<:AbstractString, <:Integer})
The body needs the element type by nameT must be boundg(m::AbstractMatrix{T}) where {T <: Integer}
# Too strict: rejects views, ranges, and other element types
bad(m::Matrix{Int}) = sum(m)

# Accepts every integer matrix, every view, every wrapped array
good(m::AbstractMatrix{<:Integer}) = sum(m)

# When the body must name the element type, bind it with where
named(m::AbstractMatrix{T}) where {T <: Integer} = (T, sum(m))

good(@view [1 2; 3 4][:, 1:1])     # 3 — a view works with the abstract signature
named([1 2; 3 4])                  # (Int64, 10)
bad(@view [1 2; 3 4][:, 1:1])      # ERROR: MethodError — a view is not a Matrix

The rule of thumb: annotate the behaviour you need (AbstractMatrix, <:Integer), not the exact type you happened to test with. When the body needs the concrete element type, name it with where.

Over-Parameterising a Design

More parameters are not more flexibility. Each one adds a dimension to the type space, multiplies the number of compiled instances, and lengthens every error message. Add a parameter only when it changes storage, dispatch, or the supertype.

# Over-parameterised: R and S are never used in a field
struct Bad2{T, R, S}
    values::Vector{T}
end

Bad2{Float64, Int, Int}([1.0, 2.0]).values    # works, and R and S mean nothing

# Lean: one parameter that drives the field
struct Good2{T}
    values::Vector{T}
end

Good2([1.0, 2.0])                             # Good2{Float64}

# A parameter that earns its place: it changes the layout
struct Sparse2{T, I <: Integer}
    indices::Vector{I}
    values::Vector{T}
end

Sparse2(Int[], Float64[])                     # Sparse2{Float64, Int64}

# Rule: a parameter should appear in a field type, in the supertype,
# or in a constraint that changes which methods apply.

Signals that a parameter is unnecessary: it appears in no field, it is always the same type in your own code, or the type behaves identically without it. Deleting it makes every signature shorter and every error message clearer.

Summary. A type parameter keeps a type concrete, so generic code compiles down to specialised code. Parameters may be types, values, or nested types, and constraints written as T <: X or Vector{<:X} narrow which instantiations may exist. Parameters are invariant — Vector{Int} is not Vector{Real} — so accept containers as AbstractVector{<:Real} rather than with a fixed element type, and bind a parameter with where only when the body needs it by name. Keep functions type-stable by seeding accumulators from T, let constructors infer parameters from their arguments, and resist adding a parameter that appears in no field.

You can now write definitions that serve every type without giving up speed. Next: Modules & Packages organises those definitions into namespaces you can load, export, and publish.