Use where clauses to tell the compiler what a type parameter can do, with interface, class, struct, new(), base class and notnull constraints, plus generic maths with static abstract members.

Bishal Biswas
Self Learner
The limitation of plain T
Inside a generic class or method, the compiler knows nothing about T except that it is a type. So this does not compile:
static T Max<T>(T a, T b)
{
return a > b ? a : b; // error: operator > cannot be applied to T
}
There is no guarantee that T supports >. A constraint tells the compiler what T must be able to do, which unlocks those members inside the method.
The where clause
Constraints go after the parameter list with where:
static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
Console.WriteLine(Max(3, 9)); // 9
Console.WriteLine(Max("pear", "apple")); // pear
// Max(new object(), new object()); // error: object is not IComparable<object>
Now T must implement IComparable<T>, so CompareTo is available, and callers can only pass types that qualify.
Interface constraints
The most common kind. Require one or more interfaces:
interface IEntity
{
int Id { get; }
}
class Repository<T> where T : IEntity
{
private readonly Dictionary<int, T> items = new();
public void Add(T item) => items[item.Id] = item; // Id is available because of the constraint
public T? Get(int id) => items.GetValueOrDefault(id);
}
Several constraints are separated by commas:
static void Process<T>(T item) where T : IEntity, IComparable<T>
Base class constraints
Require that T inherits from a class:
class Animal { public string Name { get; set; } = ""; }
class Dog : Animal { }
static void PrintNames<T>(List<T> animals) where T : Animal
{
foreach (T a in animals) Console.WriteLine(a.Name);
}
PrintNames(new List<Dog> { new() { Name = "Rex" } });
class and struct constraints
where T : class means T must be a reference type. where T : struct means a value type:
static bool IsNull<T>(T value) where T : class => value is null;
static T? FirstOrNull<T>(List<T> list) where T : struct
{
return list.Count > 0 ? list[0] : null; // T? means Nullable<T> here
}
The struct constraint is what makes T? mean Nullable<T> inside the method. Without it, T? for a struct would not compile.
The new() constraint
Allows generic code to create instances with new T(). The type must have a public parameterless constructor:
static T Create<T>() where T : new()
{
return new T();
}
static List<T> MakeMany<T>(int count) where T : new()
{
var list = new List<T>();
for (int i = 0; i < count; i++) list.Add(new T());
return list;
}
var dogs = MakeMany<Dog>(3);
new() must be the last constraint in the list when combined with others.
The notnull constraint
where T : notnull says T cannot be a nullable type. It is mostly used for dictionary keys, and it produces a warning rather than an error:
class Cache<TKey, TValue> where TKey : notnull
{
private readonly Dictionary<TKey, TValue> store = new();
}
Combining constraints
static T Clone<T>(T source) where T : class, ICloneable, new()
{
return (T)source.Clone();
}
Order: base class or class or struct first, then interfaces, then new() last.
Constraints on generic classes
Each type parameter gets its own where:
class Mapper<TSource, TTarget>
where TSource : class
where TTarget : class, new()
{
public TTarget Map(TSource source)
{
var target = new TTarget();
// copy matching properties, reflection covered later
return target;
}
}
Generic maths with static abstract members
Since .NET 7 interfaces can declare static abstract members, and the numeric types implement interfaces such as INumber<T>. This finally lets you write maths over any number type:
using System.Numerics;
static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
{
T total = T.Zero;
foreach (T v in values) total += v;
return total;
}
Console.WriteLine(Sum(new[] { 1, 2, 3 })); // 6
Console.WriteLine(Sum(new[] { 1.5, 2.5 })); // 4
Console.WriteLine(Sum(new[] { 10m, 0.5m })); // 10.5
T.Zero and += work because INumber<T> guarantees them. Before this, you needed one overload per numeric type or a slow dynamic trick.
Choosing constraints
Only add the constraints you actually use inside the code. A constraint you do not need shuts out callers for no reason. Start with none and let the compiler errors tell you which you need.
Quick recap
- Plain
Tonly exposes the members ofobject. Constraints unlock more. where T : ISomethingis the everyday constraint and lets you call interface members.class,structand a base class constrain the kind of type.new()lets generic code create instances and must come last.notnullrestricts keys and similar to non nullable types.INumber<T>and static abstract members enable generic maths.
// Generic constraints in C#
using System.Numerics;
// Interface constraint unlocks CompareTo
static T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) > 0 ? a : b;
Console.WriteLine(Max(3, 9)); // 9
Console.WriteLine(Max("pear", "apple")); // pear
// Base class + new() constraints
class Animal { public string Name { get; set; } = "unnamed"; }
class Dog : Animal { }
static List<T> MakeMany<T>(int count) where T : Animal, new()
{
var list = new List<T>();
for (int i = 0; i < count; i++)
list.Add(new T { Name = $"{typeof(T).Name} {i + 1}" }); // new T() allowed
return list;
}
foreach (var d in MakeMany<Dog>(2))
Console.WriteLine(d.Name); // Dog 1, Dog 2
// struct constraint makes T? mean Nullable<T>
static T? FirstOrNull<T>(List<T> list) where T : struct
=> list.Count > 0 ? list[0] : null;
Console.WriteLine(FirstOrNull(new List<int>()) is null); // True
Console.WriteLine(FirstOrNull(new List<int> { 7 })); // 7
// Repository restricted to entities with an Id
interface IEntity { int Id { get; } }
record Product(int Id, string Name) : IEntity;
class Repository<T> where T : IEntity
{
private readonly Dictionary<int, T> items = new();
public void Add(T item) => items[item.Id] = item; // Id is known to exist
public T? Get(int id) => items.GetValueOrDefault(id);
}
var repo = new Repository<Product>();
repo.Add(new Product(1, "Keyboard"));
Console.WriteLine(repo.Get(1)?.Name); // Keyboard
// Generic maths with INumber<T>
static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
{
T total = T.Zero;
foreach (T v in values) total += v;
return total;
}
Console.WriteLine(Sum(new[] { 1, 2, 3 })); // 6
Console.WriteLine(Sum(new[] { 1.5, 2.5 })); // 4
Console.WriteLine(Sum(new[] { 10m, 0.5m })); // 10.5
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready