Back to Blog
C#

C# Type Class: Emulating with Interfaces

c# type class: Learn how to emulate type classes in C# using interfaces, generic constraints, and static generic classes, with tradeoffs and decision criteria.

C#Type ClassesInterfacesGeneric ConstraintsAd-hoc PolymorphismFunctional Programming
Diagram showing an interface bridging a generic method and multiple type instances, representing C# type class emulation.

The phrase c# type class usually refers to the type class pattern from functional programming. C# does not have a built-in type class construct like Haskell or Scala, but you can approximate it with interfaces, generic constraints, and static generic classes. This article shows how to implement a type class–like pattern in C#, where it works well, and where it breaks down.

What a Type Class Is and Why C# Lacks It

A type class defines a set of operations that can be implemented for multiple types without modifying those types. In Haskell, you declare an instance for a type, and the compiler selects that instance based on the static type. C# uses nominal typing: an interface must be implemented by the type itself or by an adapter that wraps it. There is no language-level way to attach a new operation to an existing type after the fact without changing the type or using a separate lookup table.

This is why a direct translation of type classes is not possible in C#. The closest idiomatic equivalents are interfaces, which provide polymorphic behavior through explicit implementation, and generic constraints, which restrict type parameters to types that implement a given interface. Neither gives you the ability to add an instance for a type you do not own without a wrapper.

A Minimal Type Class Implementation with an Interface

The simplest way to emulate a type class is to define an interface that represents the operations, then provide separate implementations for each type you care about. Consider a Show type class that converts a value to a string.

public interface IShow<T> { string Show(T value); } public class IntShow : IShow<int> { public string Show(int value) => value.ToString(); } public class StringShow : IShow<string> { public string Show(string value) => value; }

These classes are not attached to int or string. They are standalone implementations that can be passed to a generic method when needed.

public static string Print<T>(T value, IShow<T> show) { return show.Show(value); }

Calling Print(42, new IntShow()) produces "42", and Print("hello", new StringShow()) produces "hello". This is explicit type class passing: the caller decides which instance to use. It works, but it requires callers to know about and supply the instance every time.

Using a Static Generic Class to Look Up Instances

To make the instance selection implicit, you can store the instance in a static generic class. The static constructor runs once for each closed generic type, so the lookup is done only once per type.

public static class Show<T> { public static Func<T, string> Instance { get; } static Show() { if (typeof(T) == typeof(int)) { Instance = value => value.ToString(); } else if (typeof(T) == typeof(string)) { Instance = value => value; } else { Instance = value => throw new NotSupportedException($"No Show instance for {typeof(T)}"); } } }

The generic method then becomes:

public static string Print<T>(T value) { return Show<T>.Instance(value); }

This removes the need to pass an instance manually. The static constructor maps each type to its implementation. However, the mapping is hard-coded. Adding a new type means editing the static constructor, which violates the open/closed principle and makes the pattern less maintainable.

A More Flexible Registry-Based Approach

If you need to support types that are not known at compile time, you can use a registry that maps Type to an instance. Because C# cannot store IShow<T> in a non-generic collection without losing type information, you store the instance as object and cast it when retrieving.

public static class ShowRegistry { private static readonly Dictionary<Type, object> _instances = new(); public static void Register<T>(IShow<T> instance) { _instances[typeof(T)] = instance; } public static IShow<T> Get<T>() { if (_instances.TryGetValue(typeof(T), out var instance)) { return (IShow<T>)instance; } throw new NotSupportedException($"No Show instance for {typeof(T)}"); } }

Registration happens at startup:

ShowRegistry.Register(new IntShow()); ShowRegistry.Register(new StringShow());

Then Print can use the registry:

public static string Print<T>(T value) { return ShowRegistry.Get<T>().Show(value); }

This approach allows external modules to register their own instances, but it introduces a global mutable state. The dictionary is not thread-safe by default, so concurrent registration or lookup can cause race conditions. You would need to synchronize access or use a concurrent collection.

Performance and Maintainability Considerations

The static generic class approach has the lowest runtime cost because the instance is resolved once per closed generic type. The registry approach adds a dictionary lookup on every call, which is still fast but not free. Reflection-based alternatives, such as using typeof(T) to find an instance at runtime, are slower and should be avoided in hot paths.

Maintainability is the bigger issue. Hard-coded type checks in a static constructor are easy to write but difficult to extend. A registry is more flexible but requires careful initialization and thread safety. In both cases, the pattern is less discoverable than a plain interface implementation, because the operations are not listed on the type itself. A developer reading a type like int will not see that a Show instance exists unless they know where to look.

When to Use This Pattern in C#

Use explicit interface instances when you have a small, fixed set of types and you want to keep the implementation obvious. Pass the instance as a parameter to the generic method. This is the most idiomatic and maintainable approach.

Use a static generic class when the set of types is known at compile time and you want to avoid passing instances manually. The mapping is centralized, but you must update it whenever a new type is added.

Use a registry when types can be added from external assemblies or plugins. This gives you extensibility at the cost of global state and synchronization.

Prefer plain interfaces when the type itself can implement the operation. If you own the type and the operation is a natural part of its behavior, implementing IShow<T> directly is simpler and more discoverable than any external type class emulation. The type class pattern is most valuable when you need to add operations to types you do not control, or when you want to provide multiple alternative implementations for the same type.

c# type class: Practical Usage and Code Examples | RYUSLOG DEV