Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Type Systems

The type system is a language’s first line of defense — catching errors before your code ever runs.

1. Static vs Dynamic Typing

Static Typing

Types are checked at compile time. Variables have declared types that don’t change.

int x = 10;
x = "hello";  // compile error!

String s = 42;  // compile error!
#![allow(unused)]
fn main() {
let x: i32 = 10;
// x = "hello";  // compile error!

fn add(a: i32, b: i32) -> i32 {
    a + b
}
}

Dynamic Typing

Types are checked at runtime. Variables can hold any type.

x = 10
x = "hello"    # fine — x just holds a different object
x = [1, 2, 3]  # also fine

def add(a, b):
    return a + b

add(1, 2)       # 3
add("hi", "!")  # "hi!"
add(1, "hi")    # TypeError at runtime
let x = 10;
x = "hello";    // fine
x = [1, 2, 3];  // fine

Comparison Table

AspectStaticDynamic
Type checkingCompile timeRuntime
Error detectionBefore runningDuring execution
PerformanceGenerally fasterGenerally slower (type overhead)
Code verbosityMore verbose (type annotations)Less verbose
Refactoring safetyCompiler catches type mismatchesTests catch them
IDE supportExcellent (autocomplete, error detection)Good but limited
FlexibilityLess flexibleMore flexible
ExamplesJava, C++, Rust, Go, TypeScriptPython, JavaScript, Ruby, Lua

Gradual Typing

Some languages support both:

// TypeScript: static typing on top of JavaScript
function add(a: number, b: number): number {
    return a + b;
}

// Can opt out with 'any'
let flexible: any = 10;
flexible = "hello";  // no error
# Python: type hints (PEP 484) — not enforced at runtime
def add(a: int, b: int) -> int:
    return a + b

add("hi", "!")  # no error at runtime! (mypy would catch it)

2. Strong vs Weak Typing

Strong Typing

The language prevents you from mixing types without explicit conversion.

# Python: strongly typed
"hello" + 42  # TypeError!
"hello" + str(42)  # "hello42" — explicit conversion required
#![allow(unused)]
fn main() {
// Rust: strongly typed
let x: i32 = 5;
let y: f64 = 3.14;
// let z = x + y;  // compile error!
let z = x as f64 + y;  // explicit cast
}

Weak Typing

The language implicitly converts types to make operations work.

// JavaScript: weakly typed
"hello" + 42     // "hello42" — number coerced to string
"5" - 3          // 2 — string coerced to number
"5" + 3          // "53" — number coerced to string (different rule!)
true + true      // 2
[] + {}           // "[object Object]"
{} + []           // 0
// C: weakly typed
int x = 5;
double y = 3.14;
double z = x + y;  // x implicitly converted to double

char c = 65;
printf("%c", c);   // prints 'A' (ASCII)

Strong vs Weak Spectrum

LanguageStrengthNotes
PythonStrongNo implicit type coercion
JavaStrongStrict, but has widening conversions
RustStrongNo implicit conversions
GoStrongNo implicit conversions
CWeakImplicit conversions everywhere
JavaScriptVery WeakAggressive coercion
PHPVery Weak"0" is falsy!

3. Duck Typing

“If it walks like a duck and quacks like a duck, it’s a duck.”

In duck-typed languages, what matters is what an object can do, not what it is.

# Python: duck typing
class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

class Duck:
    def speak(self):
        return "Quack!"

def make_it_speak(animal):
    return animal.speak()  # doesn't care about the type

make_it_speak(Dog())   # "Woof!"
make_it_speak(Cat())   # "Meow!"
make_it_speak(Duck())  # "Quack!"
// JavaScript: duck typing
function getArea(shape) {
    return shape.width * shape.height;  // just needs these properties
}

getArea({ width: 10, height: 20 });     // 200
getArea({ width: 5, height: 3, color: "red" });  // 15

Protocol / Interface Equivalents

# Python: protocols (structural typing)
from typing import Protocol

class Speakable(Protocol):
    def speak(self) -> str: ...

def make_it_speak(animal: Speakable) -> str:
    return animal.speak()

# No need to explicitly implement Speakable
# Any class with a speak() method satisfies it

4. Structural vs Nominal Typing

Nominal Typing

Types are compatible only if they have the same name (or explicit inheritance).

// Java: nominal
interface Drawable {
    void draw();
}

class Circle implements Drawable {
    public void draw() { /* ... */ }
}

class Square {
    public void draw() { /* ... */ }
}

// Square can't be used as Drawable — it doesn't implement the interface
// Even though it has the exact same method

Structural Typing

Types are compatible if they have the same structure (same methods/properties).

// TypeScript: structural
interface Drawable {
    draw(): void;
}

class Circle {
    draw() { /* ... */ }
}

class Square {
    draw() { /* ... */ }
}

function render(shape: Drawable) {
    shape.draw();
}

render(new Circle());  // works!
render(new Square());  // works! Both have draw()
// Go: structural (interfaces are implicit)
type Reader interface {
    Read(p []byte) (n int, err error)
}

type MyFile struct { /* ... */ }
func (f MyFile) Read(p []byte) (int, error) { /* ... */ }

// MyFile satisfies Reader automatically — no 'implements' keyword

Comparison

AspectNominalStructural
CompatibilityBased on declared namesBased on shape/structure
Explicit implementationRequiredNot required
RefactoringSafer (changes break at interface)More flexible
ExamplesJava, C#, Rust (traits)TypeScript, Go
Catch type errorsAt declarationAt usage

5. Generics and Templates

Generics let you write code that works with any type while maintaining type safety.

Java Generics

// Generic class
public class Box<T> {
    private T value;
    
    public Box(T value) { this.value = value; }
    public T getValue() { return value; }
}

Box<Integer> intBox = new Box<>(42);
Box<String> strBox = new Box<>("hello");

// Generic method
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    return list.stream().filter(predicate).collect(Collectors.toList());
}

// Bounded generics
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

Rust Generics

#![allow(unused)]
fn main() {
// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in &list[1..] {
        if item > largest {
            largest = item;
        }
    }
    largest
}

// Generic struct
struct Point<T> {
    x: T,
    y: T,
}

// Implementation for specific type
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}
}

C++ Templates

// Function template
template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

// Class template
template <typename T>
class Stack {
    std::vector<T> elements;
public:
    void push(T const& elem) { elements.push_back(elem); }
    T pop() {
        T elem = elements.back();
        elements.pop_back();
        return elem;
    }
};

// Template specialization
template <>
class Stack<std::string> {
    // specialized implementation for strings
};

Generics vs Templates

AspectGenerics (Java, C#)Templates (C++)
Type checkingAt definition (erasure)At instantiation
Code generationSingle implementation (type erasure)Separate code per type
Runtime costBoxing for primitivesNone
Error messagesClearOften cryptic
SpecializationLimitedFull

Go Generics (1.18+)

func Map[T any, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

// Usage
doubled := Map([]int{1, 2, 3}, func(x int) int { return x * 2 })

6. Type Inference

The compiler deduces types without explicit annotations.

#![allow(unused)]
fn main() {
// Rust: full type inference
let x = 5;           // inferred as i32
let y = 3.14;        // inferred as f64
let v = vec![1, 2];  // inferred as Vec<i32>

// Sometimes you need to help
let parsed: i32 = "42".parse().unwrap();
}
// TypeScript: type inference
let x = 5;           // inferred as number
let s = "hello";     // inferred as string
const arr = [1, 2];  // inferred as number[]

// Return type inferred
function add(a: number, b: number) {
    return a + b;  // inferred as number
}
// Go: short variable declaration infers type
x := 5           // int
y := 3.14        // float64
s := "hello"     // string
// Java: var (Java 10+)
var list = new ArrayList<String>();  // inferred as ArrayList<String>
var x = 5;                           // inferred as int

7. Type Conversion and Coercion

Implicit Conversion (Coercion)

// JavaScript coercion rules
"5" + 3     // "53" (number → string)
"5" - 3     // 2 (string → number)
true + 1    // 2 (boolean → number)
null + 1    // 1 (null → 0)
undefined + 1 // NaN

Explicit Conversion (Casting)

# Python
int("42")      # 42
float("3.14")  # 3.14
str(42)        # "42"
bool(0)        # False
bool("")       # False
bool([])       # False
// Java
// Widening (safe, implicit)
int i = 42;
long l = i;         // OK
double d = i;       // OK

// Narrowing (lossy, explicit)
double d = 3.99;
int i = (int) d;    // 3 (truncation)

Type Conversion Table

From → ToSafe?Example
int → floatfloat(5)5.0
float → int⚠️int(3.7)3 (truncation)
int → stringstr(42)"42"
string → int⚠️int("42")42, int("hi") → error
bool → intint(True)1
int → bool⚠️bool(0)False, bool(1)True

8. Algebraic Data Types

Some languages support sum types (tagged unions) in addition to product types (structs/tuples).

#![allow(unused)]
fn main() {
// Sum type (enum with data)
enum Shape {
    Circle(f64),             // radius
    Rectangle(f64, f64),     // width, height
    Triangle(f64, f64, f64), // three sides
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h) => w * h,
        Shape::Triangle(a, b, c) => {
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
    }
}
}
-- Haskell
data Shape = Circle Double
           | Rectangle Double Double
           | Triangle Double Double Double

area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h

Interview Questions

  1. What’s the difference between static and dynamic typing? Static: types checked at compile time (Java, Rust). Dynamic: types checked at runtime (Python, JavaScript). Static catches errors earlier; dynamic is more flexible.

  2. What is duck typing? An object’s suitability is determined by the presence of certain methods and properties, not by its actual type. “If it has the right methods, it works.”

  3. Explain structural vs nominal typing. Nominal: types must have the same name/inheritance (Java interfaces). Structural: types must have the same shape (TypeScript interfaces, Go interfaces).

  4. What are generics? Why use them? Generics let you write type-safe code that works with any type. They avoid code duplication while maintaining compile-time type safety.

  5. What is type erasure? In Java generics, type parameters are erased at runtime — List<String> and List<Integer> are both List at runtime. This means you can’t do instanceof checks on generic types.

  6. What’s the difference between generics and templates? Generics (Java): single implementation, type erased at runtime. Templates (C++): separate code generated for each type, no runtime cost but larger binaries and cryptic errors.

  7. Explain type inference. The compiler deduces types from context without explicit annotations. Examples: Rust’s let x = 5, TypeScript’s let x = 5, Go’s x := 5.

  8. What is an algebraic data type? A composite type formed by combining other types. Product types (AND): structs/tuples. Sum types (OR): tagged unions/enums. Rust’s enum is a sum type.