Sym Piracha

Static vs dynamic typing

When I first started programming, types were just… there. Sometimes I had to write them down (let count: number = 0), other times I could ignore them entirely (count = "whoops"), and occasionally the compiler would yell at me for mixing things up.

What I didn’t really appreciate until much later was that when and how a programming language checks types fundamentally shapes the development experience. It affects how quickly you write code, how you find bugs, how your tools help you, and even how the language runs your code under the hood.

The timing and strictness of type checking, whether performed at compile time, runtime, or a mix, determines whether a language is considered statically typed, dynamically typed, or gradually typed.

Static typing

In statically typed languages, the compiler determines and checks the types of all variables, parameters, and return values before the code is run.

When you write code in a statically typed language, the compiler typically goes through these steps before running anything:

  1. Parse the code into an Abstract Syntax Tree (AST)
  2. Build a symbol table (a map of identifiers to their types)
  3. Apply type checking rules

Static typing in action

Here is what static typing in Java looks like:

int count = 3;             // OK
// count = "oops";         // Compile-time error: incompatible types

Key characteristics

  • Better IDE support: autocomplete, navigation, and safe refactoring
    IDEs hook into the compiler’s type information, either by running the compiler in analysis mode or using the Language Server Protocol (LSP) to understand the language’s syntax and type rules.

    • Autocomplete: When you type car., the IDE checks the type of car and shows only valid methods and properties.
    • Navigation: You can jump directly to where a method or variable is defined because the IDE knows exactly what type it belongs to.
    • Safe refactoring: Renaming a method updates all correct usages without touching unrelated code.
  • Earlier bug detection: Many bugs can be caught at compile time because type rules are enforced before the program runs. These are often the bugs related to mismatched types or incorrect structure, the kind of issues that, if unchecked, could cause production crashes.

  • Type inference reduces annotation noise: “Static” doesn’t mean “write types everywhere.” Languages like Rust, Kotlin, Swift, and modern TypeScript infer many types, keeping code concise while retaining compile-time guarantees.

  • Performance optimizations: Knowing types in advance lets the compiler produce optimized machine code. This can include:

    • Inlining method calls
    • Using specialized CPU instructions
    • Eliminating unnecessary runtime type checks

The combination of earlier bug detection, richer IDE support, and performance benefits often translates to more maintainable and scalable code, especially for large projects and critical systems.

Dynamic typing

In dynamically typed languages, type checking happens at runtime, not at compile time. The interpreter (or runtime) determines the type of a value when the program is actually running, and variables themselves don’t have fixed types, only the values they hold do.

When you write code in a dynamically typed language, there’s usually no explicit type analysis phase before execution. Instead, the process looks more like:

  1. Parse the code into an Abstract Syntax Tree (AST)
  2. Immediately execute that AST (or bytecode) in an interpreter or just-in-time (JIT) compiler
  3. Perform type checks only when an operation is actually executed

Dynamic typing in action

Here is what dynamic typing in JavaScript looks like:

count = 0        // count is an int
count = "oops"   // now it's a string — no compile error

Key characteristics

  • Flexibility and speed of writing: You can start coding without thinking about type annotations or rigid declarations. Variables can change type freely.
  • Runtime overhead: Since type checks happen while the program runs, the interpreter must carry extra metadata about values and perform more checks. This can slow execution compared to optimized statically typed code. Modern JIT compilers (like V8 for JavaScript) can reduce this cost through runtime profiling and speculative optimizations.

Dynamic typing is a double-edged sword. It accelerates early development but can produce unexpected runtime errors if you don’t have strong testing in place. That’s why many teams lean on unit tests and linters to catch issues that a compiler would otherwise flag in a statically typed language.

Advocates argue that the bugs prevented by static type systems can often be caught just as effectively through testing, and that the creative flow possible in a dynamically typed language outweighs the safety net of compile-time checks.

Gradual typing

Gradual typing is a type system approach that blends static and dynamic typing within the same language. You can start without type annotations, just like in a dynamically typed language, and then add them incrementally, gaining some of the benefits of static typing where they matter most.

Under the hood, a gradually typed language treats untyped code as dynamically typed and typed code as statically typed, inserting runtime checks at the boundaries where the two interact. This lets teams choose the right balance between safety and flexibility for each part of the codebase.

Gradual typing in action

Here is what dynamic typing in TypeScript looks like:

// TypeScript: start dynamic-ish, add types later
function greet(name) {
  return `Hello, ${name.toUpperCase()}`;
}
// Later, add annotations:
function greetTyped(name: string): string {
  return `Hello, ${name.toUpperCase()}`;
}

Nuances to be aware of

  • Performance trade-offs: Crossing a typed ↔ untyped boundary can add runtime checks. Most apps won’t notice, but hot paths should be measured.
  • Tooling implications: Type checkers only verify the annotated surface. Untyped areas aren’t checked, so coverage matters.
  • Team migration value: Gradual typing is great for incrementally hardening large dynamic codebases, add types to core modules first, expand outward as you go.

In practice, gradual typing offers the best of both worlds and is most valuable when:

  • You want the rapid prototyping speed of dynamic typing early on
  • You need the scalability and maintainability of static typing as the system grows