Code of the Day

Data Types & Type Systems

How compilers and runtimes use types to catch bugs before they become crashes — primitives, static vs dynamic, strong vs weak, generics, and more.

Data Types & Type Systems12 min read
By the end of this lesson you will be able to:
  • Distinguish primitive types from composite types and explain how they are laid out in memory
  • Contrast static typing with dynamic typing and strong typing with weak typing
  • Explain what type inference does and why it does not make a language dynamically typed
  • Describe the difference between structural and nominal type systems
  • Explain what generics are and why they matter for code reuse

A type is a contract: it tells the compiler (or the runtime) what operations are legal on a value and how many bytes to reserve for it. Get the contract wrong and you get either a compile-time error or — worse — silent memory corruption at runtime. The type system is the first line of defence in any language, and understanding how it works shapes every design decision you make.

Primitives vs composite types

Primitive types are the atoms of a type system. They map directly to what the CPU can manipulate: integers, floating-point numbers, booleans, and in many languages characters. Their size is fixed and machine-level:

Type        Typical size    Range / notes
---------   ------------    --------------------------------
int8        1 byte          −128 to 127
uint32      4 bytes         0 to 4 294 967 295
float64     8 bytes         ~15 decimal digits of precision
bool        1 byte          true / false
char (UTF8) 1–4 bytes       Unicode code point

Composite types are built from primitives (and from other composites). The most fundamental are:

  • Arrays / slices — a contiguous block of same-typed elements. Random access is O(1) because the offset of element i is always base_address + i × element_size.
  • Structs / records — a fixed-layout grouping of named fields, potentially of different types. Fields are packed in declaration order, subject to alignment padding.
  • Pointers / references — a value that stores a memory address. They let you build linked structures (trees, linked lists) and share data without copying it.
  • Sum types / tagged unions — a value that is one of several possible types, with a tag byte that says which one. Rust's enum and Haskell's algebraic data types work this way.

Why does the layout matter? Because cache lines fetch 64 bytes at a time. A struct whose fields are ordered so that hot fields sit together fits in fewer cache lines and runs measurably faster than the same struct with fields scattered by size.

Static typing vs dynamic typing

In a statically typed language every expression has a type that is known at compile time. The compiler rejects programs where types are used incorrectly — before a single line of machine code runs.

// TypeScript (static)
function add(a: number, b: number): number {
  return a + b;
}
add("hello", 42);  // compile error: Argument of type 'string' is not assignable

In a dynamically typed language types are attached to values at runtime, not to variables or expressions. The same variable can hold an integer on one execution path and a string on another. Type errors surface only when bad code actually executes.

# Python (dynamic)
def add(a, b):
    return a + b

add("hello", 42)  # raises TypeError at runtime, not before

The trade-off is real. Static typing front-loads the pain (you write annotations) and eliminates whole classes of runtime errors. Dynamic typing gives faster iteration on small programs and makes certain patterns (heterogeneous collections, duck-typed interfaces) more natural — at the cost of needing thorough tests to catch what the compiler would have caught for free.

Strong typing vs weak typing

This axis is orthogonal to static/dynamic and is, frankly, less crisply defined in the literature. The working definition: a strongly typed language never coerces values to a different type implicitly; a weakly typed language will perform silent coercions to make an operation succeed.

// JavaScript (weakly typed)
console.log("5" - 3);    // 2  — string coerced to number
console.log("5" + 3);    // "53" — number coerced to string
console.log([] + {});    // "[object Object]" — both coerced to strings
# Python (strongly typed)
"5" - 3   # TypeError — no implicit coercion

Weak typing is not a flaw — C intentionally allows pointer arithmetic and unchecked casts because it operates close to hardware. But for application programming, implicit coercions are a reliable source of subtle bugs.

The terms "strong" and "weak" are often used loosely and inconsistently in online discussions. When someone says a language is "strongly typed," press them for what they mean specifically: do they mean no implicit coercions? No unchecked casts? Memory safety guarantees? The underlying properties matter more than the label.

Type inference

Type inference is a compiler's ability to deduce types from context so you don't have to write them everywhere. It does not make a language dynamically typed — the types are still resolved at compile time.

// TypeScript — type annotation omitted, but type is still inferred as number
const x = 42;       // TypeScript knows x: number
const y = x + "?";  // still a compile error: number + string
// Rust — full Hindley–Milner inference; almost never need annotations inside functions
let mut v = Vec::new();
v.push(1);   // Rust infers Vec<i32> from this line
v.push("?"); // compile error: expected i32, found &str

Inference is purely a quality-of-life feature. The type discipline is the same; you just type less.

Structural vs nominal typing

Two languages can both be statically typed yet disagree on what makes two types compatible.

Nominal typing says: type A is compatible with type B only if A explicitly declares that it extends or implements B. Java and C# work this way. The names matter.

Structural typing says: type A is compatible with type B if A has at least the same fields and methods as B — regardless of what A is named or whether it mentions B at all. TypeScript and Go work this way.

// TypeScript (structural)
interface Printable { print(): void; }

class Logger {
  print() { console.log("log"); }  // never mentions Printable
}

function show(p: Printable) { p.print(); }
show(new Logger());  // works — Logger is structurally compatible with Printable

In Go this is called "implicit interface satisfaction." A type satisfies an interface the moment it implements all of the interface's methods, with zero ceremony. Structural typing reduces boilerplate and enables retroactive composition; nominal typing makes intent explicit and avoids accidental compatibility when two unrelated types happen to share a method name.

Generics

Generics (also called parametric polymorphism) let you write a single function or data structure that works across many types without sacrificing static type-checking.

// Without generics: must duplicate for every type, or lose type safety with 'any'
function firstNumber(arr: number[]): number { return arr[0]; }
function firstString(arr: string[]): string { return arr[0]; }

// With generics: one function, still fully type-checked at each call site
function first<T>(arr: T[]): T { return arr[0]; }

const n = first([1, 2, 3]);    // T inferred as number
const s = first(["a", "b"]);   // T inferred as string

At the implementation level, languages handle generics differently. Java uses type erasure: the generic information exists only at compile time and is stripped from the bytecode, so all instantiations share a single compiled form. C++ uses monomorphisation: a separate, fully specialised copy of the code is compiled for each distinct type argument. Rust follows C++ here. Monomorphisation produces faster code (no virtual dispatch) but larger binaries.

The compiler treats a generic type parameter as "a type about which only what the constraints say is known." A constraint (or trait bound in Rust, type class in Haskell) restricts T to types that support a given set of operations — for instance, T: Ord means T supports comparison.

The type system is, in a real sense, a formal proof system embedded in the compiler. When a Rust program compiles, you have a machine-checked proof that the code does not contain data races or use-after-free errors. The annotations you write are the premises; the type-checker is the proof assistant.

Where to go next

The type system interacts directly with how values are stored and passed through the machine. The Computer Architecture track covers how registers, cache, and memory hierarchy affect the cost of the operations the type system permits. For a practical application of type-system thinking, see the Rust track, which has one of the most expressive type systems in mainstream use.

Knowledge check

  1. 1.
    A language where every variable's type is resolved before any code runs is best described as:
  2. 2.
    A language that uses type inference (like Rust or TypeScript) is therefore dynamically typed.
  3. 3.
    Which of the following are true of structural typing?
Finished reading? Mark it complete to track your progress.

On this page