Type Systems
Static vs. dynamic typing, assignability, covariance, boxing, and top/bottom types
Static vs. dynamic typing
| Dynamic (Python, JavaScript) | Static (Java, CatScript) | |
|---|---|---|
| When checked | Runtime | Compile time |
| Variable reassignment | Any type, anytime | Must be compatible with declared type |
| Tooling | Limited autocomplete | Full autocomplete, refactoring, x. shows available methods |
| Maintenance | Tests needed for safety | Compiler catches type mismatches |
| Speed of writing | Fast - no fighting the type system | More overhead, but type inference (var) helps |
// JavaScript - perfectly fine
let x = 10;
x = "foo";
console.log(x.length); // 3
x = [1, 2, 3];
console.log(x.length); // 3
// Java - compilation error
var x = 10;
x = "foo"; // ERROR: incompatible types
Dynamic typing uses duck typing - if it walks like a duck and quacks like a duck, it’s a duck. Static typing requires compile-time guarantees about what types are.
The core question: is X assignable to Y?
Almost every type system operation comes down to assignability:
var x = 10;
x = "foo"; // Is String assignable to int? No -> error
Object x = 10;
x = "foo"; // Is String assignable to Object? Yes -> fine
The Java type system exposes this directly:
String.class.isAssignableFrom(Object.class); // false
Object.class.isAssignableFrom(String.class); // true
Downcasting
When a variable’s compile-time type is too broad, you can downcast:
Object x = "hello";
System.out.println(((String) x).length()); // works at runtime
But downcasts can fail:
Object x = List.of(1, 2, 3);
System.out.println(((String) x).length()); // ClassCastException at runtime!
The Java type system can be subverted through downcasting. Class cast exceptions are the symptom.
Java arrays are covariant (and unsound)
String[] strings = {"a", "b", "c"};
Object[] objects = strings; // legal - arrays are covariant
objects[0] = "hello"; // fine - String assignable to Object
objects[1] = true; // compiles! but: ArrayStoreException at runtime
Covariance means: if String is assignable to Object, then String[] is assignable to Object[].
This is convenient - you can write a single printArray(Object[] values) method that works with any array type. But it’s unsound because you can insert incompatible types through the wider reference.
In practice, arrays are rarely mutated, so ArrayStoreException is almost never seen.
Java generics are invariant (but also broken)
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // ERROR: incompatible types
Generics learned from the array mistake - List<String> is not assignable to List<Object>.
But raw type casts bypass everything:
List<String> strings = new ArrayList<>();
List rawList = (List) strings;
rawList.add(true); // no runtime error - generics are erased!
Java generics give the worst of both worlds: inconvenient invariance, and type safety can still be broken through raw types. The erasure-based implementation means the JVM doesn’t even know the generic type at runtime.
Primitive vs. reference types
Primitives (int, boolean, float) |
Reference types (String, List, Integer) |
|
|---|---|---|
| Storage | Value stored directly (on the stack) | Pointer to heap-allocated object |
| Nullable | Cannot be null |
Can be null |
| Performance | Fast - no pointer chasing | Slower - heap allocation + indirection |
Boxing and autoboxing
Java automatically converts between primitives and their reference wrappers:
int x = 10;
Integer y = x; // autoboxing: equivalent to Integer.valueOf(x)
int z = y; // unboxing: equivalent to y.intValue()
The danger:
Integer y = null;
int z = y; // NullPointerException! - unboxing null
This produces a NullPointerException on a line with no method call, which is confusing unless you understand autoboxing.
Top type and bottom type
Type systems form a hierarchy. Two special types anchor the extremes:
| Type | Definition | Java | CatScript |
|---|---|---|---|
| Top | Every type is assignable to it | Object |
object |
| Bottom | It is assignable to every type | null type |
null |
Object (top - everything assignable to it)
/ | \
String Int List
\ | /
null type (bottom - assignable to everything)
You can assign null to any reference type variable in Java. The null literal has a special type (you can’t declare a variable as null type) that is assignable to all reference types.
Practical implications
The type system is a set of rules for assigning types to programming constructs and enforcing constraints. CatScript adopts these rules:
- Statically typed - all types known at compile time
- List types are covariant on their component type (convenient but unsound)
nullis the bottom type (assignable to everything)objectis the top type (everything assignable to it)- Type inference supported (
var x = 10->xisint)
Tooling > correctness?
Static type systems enable powerful tooling - type x. and see every available method. Refactoring tools, jump-to-definition, and inline documentation all depend on static type information.
The correctness guarantees matter, but the tooling is arguably the biggest practical advantage of static typing.
Summary
| Concept | Key point |
|---|---|
| Dynamic vs. static | Runtime checking vs. compile-time checking |
| Assignability | The fundamental question - is X assignable to Y? |
| Covariance | String[] assignable to Object[] - convenient but unsound |
| Generics | Invariant (List<String> not assignable to List<Object>) but still breakable via raw types |
| Primitives vs. references | Direct values vs. heap pointers; autoboxing bridges them |
| Top type | Object / object - everything assignable to it |
| Bottom type | null type - assignable to everything |
| Boxing pitfall | Unboxing null causes NullPointerException on innocent-looking lines |