📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is the Common Type System (CTS), and how does it let different .NET languages share types?

The CTS rules for value and reference types, members, inheritance and how language keywords map to CTS types.

Mid-Level Theory Intermediate clr cts type-system value-types

🎯 Key points

  • The CTS is the ECMA-335 type model every .NET language compiles to
  • Value types derive from System.ValueType and copy by value; reference types are accessed by reference
  • Enums, delegates and arrays have special base types (System.Enum, System.MulticastDelegate, System.Array)
  • Language keywords such as int and string are aliases for CTS types
  • Generics are reified in the CTS, unlike Java's erasure
  • The CLS is the subset of the CTS that all languages must support

⏱ 30-second answer

The CTS is the type model defined by ECMA-335 that every .NET language compiles to. It defines value types and reference types, the members a type can have (fields, methods, properties, events, nested types), accessibility, single class inheritance with multiple interfaces, and that everything ultimately derives from System.Object. C# int is just an alias for the CTS type System.Int32, which is why a C# library can be used from F# or VB without conversion.

🗣 Full answer (senior)

How I would answer this in an interview

The Common Type System (CTS) is the CLR's specification of what types are and how they behave: value and reference types, classes, interfaces, structs, enums, delegates, members, inheritance, visibility, and how they're represented in metadata. Every .NET language compiles to the same CTS types, which is why a class written in F# or VB.NET can be used, inherited and called from C# with no glue code.

1. What the CTS defines

  • Two categories of types:
    • Value types (derive from System.ValueType): structs, enums, primitives (int, double, bool); stored inline; copied by value.
    • Reference types: classes, interfaces, delegates, arrays, strings; stored on the heap; variables hold references.
  • Single root: everything derives from System.Object (value types can be boxed to it).
  • Type kinds: classes, interfaces, value types, enums, delegates, arrays, pointers, generic types and methods.
  • Members: fields, methods, properties (as accessor methods + metadata), events, constructors, nested types.
  • Rules: single class inheritance, multiple interface implementation, visibility (public, private, family/protected, assembly/internal, etc.), virtual/abstract/sealed, static members, generics with constraints and variance.
  • Type identity: a type is identified by its assembly + namespace + name (and generic arguments).

2. How it enables cross-language types

// F#
type Money(amount: decimal, currency: string) =
    member _.Amount = amount
    member _.Currency = currency
// C# (referencing the F# assembly)
var m = new Money(10m, "AUD");
Console.WriteLine(m.Amount);
  • Each compiler maps its language constructs to CTS types and emits metadata describing them.
  • Other compilers read that metadata and present the types in their own syntax.
  • The CLR enforces the CTS at run time (type loading, verification, casting), so all languages share one type system: an int in C# is the same System.Int32 as Integer in VB.NET.

3. Language features vs CTS features

Not every language feature exists in the CTS, and vice versa:

  • C# properties are CTS methods (get_X, set_X) plus property metadata.
  • C# records are classes with compiler-generated members — the CTS sees an ordinary class.
  • C# nullable reference types are attributes (NullableAttribute), not CTS types.
  • dynamic is object + attributes.
  • Tuples' element names are attributes; the type is ValueTuple<...>.
  • The CTS supports things C# doesn't expose directly (e.g. some IL features, modreq/modopt).

The CLS (Common Language Specification) is the subset of the CTS that all languages agree to support for interoperable public APIs.

4. Value types vs reference types in practice

Value type Reference type
Storage Inline (stack, inside objects/arrays) Heap; variable holds reference
Assignment Copies the value Copies the reference
Null Not null (use Nullable<T>) Can be null
Inheritance Sealed, derive from ValueType Single inheritance
Equality default Field-wise (ValueType.Equals) Reference
Boxing Needed to treat as object/interface N/A

These CTS rules drive performance and correctness decisions (structs vs classes, boxing, equality).

5. A production example

A team shared a domain library written in F# (discriminated unions for pricing rules) with C# services. Because both compile to CTS types, C# consumed the F# types directly; but F# unions appeared as class hierarchies with compiler-generated members (IsVolumeDiscount, Item properties), and F# options as FSharpOption<T> — awkward in C#. We added a thin C#-friendly facade (records and T? returns) at the boundary. Understanding that "cross-language" means "shared CTS types, not shared idioms" made the boundary design obvious.

🔍 What the interviewer is checking

The interviewer checks whether you understand the runtime type model behind C# syntax.

Green flags: value versus reference types described by semantics, base types for enum, delegate and array, keywords as aliases, reified generics, link to CLS.

Red flags: stack versus heap as the only distinction; thinking int and Int32 differ; unaware that generics are a runtime feature.

📚 Learn in depth

1. CTS in metadata

Type definitions live in metadata tables: TypeDef (flags for class/interface/sealed/abstract/sequential layout...), Field, MethodDef, Property, Event, InterfaceImpl, GenericParam, NestedClass. The CLR's type loader reads these and builds runtime structures (MethodTable, EEClass). Type flags encode CTS semantics like Sealed, Abstract, Interface, visibility, layout (Auto, Sequential, Explicit).

2. Value types in the CTS

Value types are sealed classes deriving from System.ValueType (enums from System.Enum) in metadata. The CLR treats them specially: unboxed instances have no object header; boxing creates a heap object with a MethodTable and copies the value. Primitive types (int32, float64) have dedicated IL encodings but map to System.Int32 etc.

3. Generics in the CTS

Generics are first-class in the CTS (since .NET 2.0): generic type definitions, instantiations, constraints, variance (+T, -T in IL for interfaces/delegates). Reified generics mean List<int> and List<string> are distinct runtime types.

4. Type equivalence and forwarding

  • Types are identified by assembly + name; moving a type between assemblies would break consumers — [TypeForwardedTo] redirects references (used heavily in .NET's reference assemblies and netstandard).
  • COM interop supports type equivalence ([TypeIdentifier]) for embedded interop types.

5. Pointer and function pointer types

The CTS includes unmanaged pointers (int32*), managed pointers (int32& — ref in C#), and function pointers (method ...*), which C# exposes via unsafe code and delegate*.

Try it yourself

  1. Inspect a C# record in ILSpy and note it's a normal class in metadata.
  2. Reference an F# or VB.NET library from C# and inspect how its types appear.
  3. Use typeof(int).BaseType and typeof(DayOfWeek).BaseType.
  4. Look at TypeDef flags with System.Reflection.Metadata.

💻 Code

Console.WriteLine(typeof(int) == typeof(Int32));            // True: int is an alias
Console.WriteLine(typeof(Point).BaseType);                  // System.ValueType
Console.WriteLine(typeof(Status).BaseType);                 // System.Enum
Console.WriteLine(typeof(Func<int>).BaseType);              // System.MulticastDelegate
Console.WriteLine(typeof(int[]).BaseType);                  // System.Array
Console.WriteLine(typeof(IDisposable).BaseType is null);    // True: interfaces have no base type

// Value semantics versus reference semantics
var p1 = new Point(1, 2);
var p2 = p1;                // copy of the value
p2.X = 10;
Console.WriteLine(p1.X);    // 1

var o1 = new Box { Value = 1 };
var o2 = o1;                // copy of the reference
o2.Value = 10;
Console.WriteLine(o1.Value); // 10

// Boxing: the struct is copied into a heap object
object boxed = p1;
Console.WriteLine(boxed.GetType()); // Point

struct Point(int x, int y) { public int X = x; public int Y = y; }
enum Status { Pending, Shipped }
class Box { public int Value; }

⚠ Common mistakes

1. Confusing CTS with CLS

What happens: using the terms interchangeably. Why it is wrong: CLS is a subset of CTS for cross-language public APIs. Do instead: explain both and their relationship.

2. Assuming language features exist in the runtime

What happens: expecting nullable reference types or tuple names at run time. Why it is wrong: they're compiler metadata, not CTS types. Do instead: know which features are compile-time only.

3. Saying "value types live on the stack"

What happens: oversimplified CTS explanation. Why it is wrong: value types live inline wherever their container is. Do instead: explain inline storage vs references.

4. Exposing language-specific idioms across boundaries

What happens: F# options or unions exposed directly to C# consumers. Why it is wrong: awkward APIs despite shared types. Do instead: design boundary-friendly APIs.

5. Ignoring boxing implications

What happens: value types treated as objects in hot paths. Why it is wrong: allocations and copies. Do instead: use generics and avoid boxing.

6. Thinking type identity is just the name

What happens: expecting same-named types in different assemblies to be equal. Why it is wrong: identity includes the assembly. Do instead: share types via a common assembly or type forwarding.

7. Overlooking reified generics

What happens: assuming Java-style erasure. Why it is wrong: .NET generics are distinct runtime types. Do instead: use typeof(T) and generic statics knowingly.

8. Treating properties as fields

What happens: assuming properties are storage in the type system. Why it is wrong: they're methods; changing field ↔ property breaks binary compatibility. Do instead: expose properties from the start in public APIs.

🏛 Senior insight: trade-offs

The CTS matters most when you cross a boundary: another language, reflection, serialisation or interop.

Boundary CTS fact that matters
F# or VB consuming a C# library Only CTS (and ideally CLS) constructs travel
Reflection and serialisers Properties and events are metadata over methods
Interfaces on structs Calling through the interface boxes unless a generic constraint is used
Generic performance Reified generics give value types specialised code

A senior engineer uses this to predict allocations and API compatibility instead of memorising definitions.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says the CTS lets languages share types Why: Checks they know the CLS is the subset every language supports
    How to answer

    The CLS is the subset of the CTS that every .NET language is expected to support, so a CLS-compliant public API can be used from C#, F#, VB and others. Typical rules: no unsigned types such as uint in public signatures, no public identifiers that differ only by case, no pointer types in public members. Adding [assembly: CLSCompliant(true)] makes the C# compiler warn (CS3001, CS3002, CS3005 and others) when a public member breaks the rules. Only the public surface matters; internal code can use anything.

  2. When: Candidate mentions value types becoming objects Why: Probes boxing cost and how to avoid it
    How to answer

    Boxing wraps a value type in a new heap object so it can be used as object or an interface; unboxing checks the object's type and copies the value back, and it must be the exact type or you get InvalidCastException. Every box is a heap allocation, so in hot paths it creates GC pressure. Hidden boxing comes from non-generic APIs like ArrayList and params object[], passing structs as interfaces, calling GetType() or non-overridden Equals/GetHashCode on structs, and enumerating a List<T> through IEnumerable<T>. I find it with BenchmarkDotNet's MemoryDiagnoser or an allocation profiler and fix it with generics, constraints and IEquatable<T>.

  3. When: Candidate says generics are reified Why: Checks knowledge of shared versus specialised generic code
    How to answer

    Generics are usually zero-cost, but how depends on T. For value types the JIT generates separate, fully specialised code, so there is no boxing and typeof(T) == typeof(int) folds to a constant. For reference types all instantiations share one canonical body, which looks up type-specific data such as statics and typeof(T) through a runtime dictionary. new T() and generic virtual methods take slower paths. The cost of specialisation is code size and JIT time, which matters for Native AOT, where every instantiation must be known at build time.

🛠 Real-world problems: how you handle them

F# domain library consumed by a C# API without a translation layer
Situation
A pricing team wrote rules in F# while the public API was C# ASP.NET Core.
Signals
The team planned a DTO mapping layer because they assumed F# types could not be used from C#.
Investigation
Inspected the F# assembly in ILSpy and confirmed records and discriminated unions compiled to ordinary CTS classes with properties.
Resolution
Referenced the F# project directly; exposed only CLS-friendly types on the boundary and kept discriminated unions internal behind simple result types.
Prevention
Added a boundary review rule: public types shared across languages must be plain CTS classes, records or interfaces.

How to tell it (STAR)

On [the pricing service], we wanted F# for rules but C# for the API. I showed the team in ILSpy that F# records are ordinary CTS classes, so we referenced the library directly and removed [a planned mapping layer of about N classes]. We kept only CLS-friendly types on the boundary.

↩ Usually asked after

More in CLR Fundamentals

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me