Generic Constraints

Vaibhav • September 11, 2025

In the previous article, we explored how generic methods allow us to write reusable logic that works across many types. But sometimes, you don’t want to allow just any type. You may want to restrict the types that can be used with your generic class or method-for example, only allowing types that implement a specific interface, or only allowing reference types. That’s where generic constraints come in.

Why Constraints Matter

Generics are powerful because they’re flexible. But that flexibility can lead to problems if you assume certain capabilities that the type might not have. For example, if you want to call a method like ToString() or access a property like Name, you need to be sure the type supports it. Constraints let you enforce those expectations at compile time.

// Without constraints
public static void PrintName<T>(T item)
{
    Console.WriteLine(item.Name); // ❌ Compile error: T might not have a Name property
}

The compiler doesn’t know what T is, so it can’t guarantee that Name exists. You need a constraint to fix that.

Using Interface Constraints

The most common constraint is requiring that a type implements a specific interface. This allows you to call methods and access members defined in that interface.

public interface IHasName
{
    string Name { get; }
}

public static void PrintName<T>(T item) where T : IHasName
{
    Console.WriteLine(item.Name); // ✅ Safe: T must implement IHasName
}

Now the compiler knows that item has a Name property, because T is constrained to types that implement IHasName.

You can constrain a type to multiple interfaces by separating them with commas: where T : IHasName, IComparable.

Constraining to Base Classes

You can also constrain a type to inherit from a specific base class. This is useful when you want to work with a known hierarchy.

public class Animal
{
    public void Speak() => Console.WriteLine("Animal sound");
}

public static void MakeSound<T>(T creature) where T : Animal
{
    creature.Speak(); // ✅ Safe: T must be an Animal
}

This method works with any type that inherits from Animal, including subclasses like Dog or Cat.

Constraining to Reference or Value Types

Sometimes you want to restrict a generic to only reference types or only value types. C# provides two special constraints for this:

// Reference type constraint
public void SaveObject<T>(T obj) where T : class
{
    // T must be a reference type
}

// Value type constraint
public void SaveValue<T>(T value) where T : struct
{
    // T must be a value type
}

These constraints help you avoid issues like nullability or boxing. For example, you might want to ensure that a type can be null (reference type) or that it’s stored efficiently (value type).

You cannot use both class and struct constraints together-they are mutually exclusive.

Constraining to Types with a Parameterless Constructor

Sometimes you need to create an instance of a generic type using new. But not all types have a parameterless constructor. To enforce that, use the new() constraint.

public static T CreateInstance<T>() where T : new()
{
    return new T(); // ✅ Safe: T must have a public parameterless constructor
}

This is useful for factories, builders, or any method that needs to instantiate a generic type.

You can combine new() with other constraints, but it must come last: where T : class, new().

Combining Multiple Constraints

You can apply multiple constraints to a single type parameter. This gives you fine-grained control over what types are allowed.

public static void Process<T>(T item)
    where T : class, IHasName, new()
{
    Console.WriteLine(item.Name);
    var copy = new T(); // ✅ Safe: T is a reference type, implements IHasName, and has a parameterless constructor
}

This method ensures that T is a reference type, implements IHasName, and can be instantiated with new.

Constraints in Generic Classes

Constraints aren’t limited to methods-you can also apply them to generic classes. This ensures that all methods and properties within the class can safely assume the constraint.

public class Repository<T> where T : IEntity
{
    public void Save(T entity)
    {
        Console.WriteLine($"Saving entity with ID: {entity.Id}");
    }
}

Here, T must implement IEntity, so the class can safely access Id or other members defined in that interface.

Constraints and Inheritance

When a generic class has constraints, those constraints apply to all derived classes unless overridden. You can also add additional constraints in derived classes.

public class BaseProcessor<T> where T : class
{
    public virtual void Process(T item) { }
}

public class AdvancedProcessor<T> : BaseProcessor<T> where T : class, IProcessable
{
    public override void Process(T item)
    {
        item.Execute(); // ✅ Safe: T must implement IProcessable
    }
}

This pattern lets you build layered abstractions with increasing specificity.

Constraints and LINQ

Constraints are especially useful when working with LINQ. For example, if you want to sort a list of generic items, you can constrain them to IComparable:

public static T FindMax<T>(List<T> items) where T : IComparable<T>
{
    return items.Max(); // ✅ Safe: T must be comparable
}

Without the constraint, Max() would fail because the compiler wouldn’t know how to compare the items.

Common Mistakes with Constraints

Constraints are powerful, but they can be misused. Here are some common mistakes to avoid:

  • Forgetting to add a constraint when calling methods or accessing properties.
  • Using incompatible constraints (e.g., class and struct together).
  • Assuming a type has a constructor without using new().
  • Constraining to interfaces that aren’t implemented by the intended types.

Always think about what capabilities your generic type needs. Use constraints to enforce those capabilities early-before bugs sneak in.

Summary

Generic constraints let you restrict the types that can be used with your generic classes and methods. They ensure type safety, enable specific operations, and improve code clarity. You’ve learned how to use constraints with interfaces, base classes, reference/value types, and constructors. You’ve also seen how to combine constraints and apply them in real-world scenarios like LINQ and inheritance. In the next article, we’ll dive into Generic Collections Deep Dive-exploring how collections like List<T>, Dictionary<K,V>, and HashSet<T> use generics under the hood to deliver performance and flexibility.