<Back

Type Systems

Many programming languages have types built-in to their syntax, and there’s different ways to look at these type systems and categorize them. Since I’ve been dabbling with a whole bunch of programming languages lately, I wanted to document them here for reference purposes…

Static vs Dynamic

Static typing and dynamic typing refer to when type information is checked and enforced in a programming language.

In statically typed languages, the variable types are determined at compile-time, before the code is executed. This means that type errors are caught early, during compilation.

  • Variables are typically bound to a specific type when they are declared, and this type cannot change during the program’s execution.
  • This can lead to more predictable and optimized code since the compiler knows the exact types of all variables ahead of time.
  • Examples of statically typed languages include Java, C++, C#.

In dynamically typed languages, the variable types are determined at runtime, while the code is being executed. This means that type errors are caught during the execution of the program.

  • Variables can hold values of any type, and their type can change during the program’s execution.
  • This can lead to more flexible and concise code, but there’s a potential for runtime type errors if not careful.
  • Examples of dynamically typed languages include Python, JavaScript, Ruby, and PHP.

Strong vs Weak

In strongly typed languages, once a variable is declared or defined with a certain data type, you typically cannot use it as if it were another type without an explicit conversion or cast.

  • Operations on incompatible types will result in a type error.
  • This can prevent unintended and potentially dangerous operations.
  • Examples of strongly typed languages include Java, C#, and Haskell.
String text = "123";
int number = text;  // This will result in a compile-time error.

In weakly typed languages, variables can be used as if they were of different types without explicit conversion or casting.

  • The language often performs implicit type coercion, converting data types behind the scenes.
  • This can lead to unexpected behaviours if not used carefully.
  • Examples of weakly typed languages include JavaScript and PHP.
let text = '123';
let number = text * 1; // This will implicitly convert the string "123" to the number 123.

Strong vs. weak typing is orthogonal to static vs. dynamic typing.

Static vs. dynamic refers to when type information is checked (compile-time vs. runtime), while strong vs. weak refers to how strictly types are enforced. For instance, Python is dynamically typed (types are checked at runtime) but is considered strongly typed because you can’t, for example, add a string to an integer without an explicit conversion.

x = "hello" + 5
> TypeError: can only concatenate str (not "int") to str
# x = "hello" + str(5) to make this work

Manifest vs Inferred

Manifest means the programmer explicitly declares the type of a variable when it’s defined. Languages such as C or Java can be considered manifest typed.

int number = 42;
String text = "Hello, World!";

Inferred means the compiler infers type by value, just like in Python, Haskell, OCaml, Typescript, Scala, and more…

let text = 'Hello, World!';

Nominal vs Structural

Nominal typing cares about the name or identity of the type.

  • In nominal typing, two variables are type-compatible if and only if their declarations name the same type.
  • The name of the type is what’s important, not the actual structure or content of the type.
  • Many object-oriented languages, like Java and C++, use nominal typing for their class systems. In these languages, two classes are different types even if they have the exact same properties and methods.

Example in TypeScript (which supports both nominal and structural typing):

class Person {
  name: string;
}

class Animal {
  name: string;
}

let person: Person;
let animal: Animal;

// This will be a type error, even though Person and Animal have the same structure.
person = animal;

Structural typing cares about the shape or structure of the type.

  • In structural typing, two variables are type-compatible if their types have the same structure.
  • The actual names of the types are irrelevant; what matters is that the structures match.
interface Duck {
  quack(): string;
}

class MyDuck {
  quack() {
    return 'Quack!';
  }
}

let duck: Duck = new MyDuck(); // This is valid because MyDuck has the same structure as the Duck interface.

In Python, we have “duck typing”, the term stemming from the saying, “If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.” In programming, this means that an object’s suitability is determined by the presence and behaviour of its methods and properties, rather than by its actual type.

What this means is that we don’t check for an object’s type explicitly; instead, we assume it has certain methods or properties. If an object is used in a context where a specific method or property is expected, and it doesn’t have that method or property, a runtime error occurs.

Examples of languages that use duck typing include Python, Ruby, and JavaScript.

def quack(duck):
    return duck.quack()

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

class Dog:
    def bark(self):
        return "Woof!"

# This will work because the Duck class has a quack method.
print(quack(Duck()))

# This will raise a runtime error because the Dog class doesn't have a quack method.
print(quack(Dog()))

Structural typing is more formalized than duck typing and is checked at compile-time in statically-typed languages