Structural vs Nominal Typing: C# Type System Explained

Type systems govern how programming languages classify values and enforce rules around data operations. The two foundational approaches to type compatibility are nominal typing, which relies on explicit names and declarations, and structural typing, which evaluates compatibility based on the shape and contents of the data. This article explains the definitions and trade-offs of both systems and examines where C# falls on the spectrum between nominal and structural design.

Understanding Nominal Typing

Nominal typing (also known as nominative typing) determines type equivalence and compatibility strictly through explicit declarations and names. Under this model, two types are not interchangeable simply because they share identical fields, properties, or methods; they must share an explicit lineage, such as inheriting from a common base class or implementing a common interface.

Languages like Java, C++, and traditional C# rely primarily on nominal typing. If you define a class named Point with properties X and Y, and another class named Vector with identical properties X and Y, the compiler treats them as entirely distinct types. You cannot pass a Vector to a function expecting a Point unless an explicit conversion or relationship is defined.

The primary advantage of nominal typing is safety and intentionality. It prevents unintentional domain overlap, ensuring that two functionally identical but semantically distinct types (such as UserId and OrderId, both containing a single integer) are never mixed up by accident.

Understanding Structural Typing

Structural typing determines type equivalence based on the actual structure, shape, and members of a type, rather than its name or formal declaration. Under a structural type system, Type A is compatible with Type B if Type A possesses all the members (with matching types) that Type B requires.

Languages like TypeScript and Go are prominent examples of structural typing. In TypeScript, if an interface defines a contract requiring a method log(): void, any object possessing a log(): void method satisfies that contract automatically, without needing to explicitly state implements Logger.

The main benefit of structural typing is flexibility and reduced coupling. Components can interact seamlessly without sharing a common dependency or inheriting from a centralized interface definition, making testing, mocking, and API composition straightforward. However, the risk is accidental compatibility, where two structurally identical types designed for completely different domain concepts can be substituted for one another.

Where C# Stands

C# is fundamentally a statically typed, nominally typed language. At its core, classes, structs, records, and interfaces require explicit declarations to establish compatibility. If ClassA and ClassB have the exact same members, the C# compiler will still reject assigning an instance of ClassA to a variable of type ClassB unless an explicit conversion operator is provided.

However, modern C# incorporates several structural and "duck-typed" features to enhance developer ergonomics:

  • Compiler Pattern-Based Operations: C# relies on structural conventions for certain language features rather than nominal interfaces. For example, a foreach loop does not strictly require an implementation of IEnumerable; it only requires the type to contain a public parameterless GetEnumerator() method that returns an object with a MoveNext() method and a Current property. Similar structural rules apply to the await keyword (which requires a GetAwaiter() method) and pattern-based Dispose() in ref struct types.
  • Tuple Types: C# tuples are structurally typed. A tuple (int, string) is completely equivalent to any other tuple of (int, string), regardless of the names given to the elements.
  • Anonymous Types: Anonymous types within the same assembly share compatibility based on property names and types, evaluated structurally by the compiler.
  • Target-Typed Object Creation and Records: While records are nominally typed classes or structs under the hood, features like target typing and value-based equality provide a developer experience that closely mirrors structural data modeling.

Ultimately, C# remains anchored in nominal typing for robust type safety and architecture, while selectively adopting structural typing patterns where flexibility and performance benefit the developer.