Learning outcome
By the end of this lesson, you should be able to explain that a generic constraint is a contract between the generic API and the caller: the type argument must satisfy the declared capabilities before the compiler allows the code to be used. You will also be able to read common constraints such as where T : class, where T : struct, where T : notnull, where T : new(), and interface or base-class constraints, and understand why they unlock specific members or behaviors inside the generic body.
Intuition
Generics answer the question, “How can I write one algorithm that works for many types?” Constraints answer the follow-up question, “What must those types be able to do?” Without constraints, a generic member only knows about object-level operations. With constraints, the compiler lets you use more specific members safely.
Think of a constraint as a doorway: if T must be comparable, the compiler permits comparison logic. If T must have a public parameterless constructor, the compiler permits object creation. If T must be a value type, the compiler can enable APIs that depend on stack-friendly or nullable-specific semantics.
Deep dive
A generic constraint is declared with a where clause after the type parameter list. The constraint expresses a requirement that must be true for every type argument supplied by the caller. This is not runtime reflection, and it is not a comment for humans only; it is part of the type system.
The most common categories are:
- Reference-type and value-type constraints:
class,class?,struct,notnull - Constructor constraint:
new() - Interface constraints:
where T : IComparable<T> - Base-class constraints:
where T : SomeBase - Specialized constraints such as
unmanaged, which are useful when APIs need unmanaged-layout guarantees
A few rules matter in practice:
- The
whereclause comes after the type parameter list. - A base-class constraint, if present, must appear before other constraints for the same type parameter.
new()appears last in the constraint list.structandunmanagedimply a value type and cannot be combined withclass.- Constraints are compile-time guards; they do not check values at runtime.
The biggest benefit is capability-based design. If a type parameter is constrained to IComparable<T>, your code can call CompareTo. If it is constrained to new(), your code can construct instances. If it is constrained to a base class, you can access members of that base class without downcasting.
A helpful mental model is: the constraint says what the generic code is allowed to assume. The compiler then enforces that assumption at the call site.
Failure modes
The most common mistake is using a type parameter as if it were more capable than it actually is. For example, calling CompareTo on unconstrained T fails because object does not guarantee that member. Another frequent issue is choosing the wrong constraint: class is about reference types, but it does not imply nullable safety in a nullable-enabled project; notnull is a separate promise.
Other pitfalls include:
- Forgetting
new()when you need to createT - Assuming
structandunmanagedare interchangeable; they are not - Using a base-class constraint when an interface constraint would be more flexible
- Over-constraining an API so it becomes hard to reuse
- Expecting
notnullviolations to behave like normal errors; in nullable-enabled code they are typically warnings, not always errors
Interview drill
- Why does a generic method need a constraint before it can call a method on
T? - When would you choose an interface constraint over a base-class constraint?
- What capability does
new()add, and why does it not work with every type? - How is
notnulldifferent fromclass? - Why can constraints improve both correctness and API design?
Revision checklist
- I can explain that constraints describe the capabilities a type argument must provide.
- I can identify common constraints and what they permit.
- I understand that constraints are checked by the compiler, not discovered at runtime.
- I can choose between interface, base-class,
struct,class,notnull, andnew()constraints based on the API need. - I can describe at least one bug that a constraint prevents.
Production code
In real APIs, constraints let you write smaller, safer abstractions. A repository factory may require new() to create entities, a comparer utility may require IComparable<T>, and a serialization helper may require an interface such as IFormattable or a custom contract. The goal is not to “decorate” the generic; it is to document and enforce the exact abilities the generic implementation depends on.
The executable example below demonstrates two common patterns: a comparison helper that requires IComparable<T>, and a factory helper that requires new(). The program also includes a deliberately constrained logger that accepts only reference types to show how class changes the accepted inputs.
Code walkthrough
Max<T> is constrained with IComparable<T>, which is why CompareTo is legal inside the method. Without that constraint, the compiler would not know whether T supports ordering. CreateDefault<T> uses new() so it can call new T() safely. Without new(), there is no guarantee that T has an accessible parameterless constructor.
PrintReferenceType<T> uses class to limit the method to reference types. That constraint communicates design intent: the method is written for reference semantics and excludes value types at the type-system boundary. Notice that the code does not need casts or reflection; the compiler enforces the promise directly.
A good interview answer should emphasize this final idea: constraints do not make code “more generic” by themselves. They make generic code more honest about its dependencies, which is what allows the compiler to protect both the implementation and its callers.
Executable code examples
Generic constraints in action
Program.cs
using System;
public static class Program
{
public static void Main()
{
var biggestNumber = Max(42, 17);
var biggestWord = Max("alpha", "omega");
Console.WriteLine(biggestNumber);
Console.WriteLine(biggestWord);
var widget = CreateDefault<Widget>();
Console.WriteLine(widget.Name);
PrintReferenceType("hello");
PrintReferenceType(widget);
Console.WriteLine("constraints-ok");
}
public static T Max<T>(T left, T right) where T : IComparable<T>
=> left.CompareTo(right) >= 0 ? left : right;
public static T CreateDefault<T>() where T : new()
=> new T();
public static void PrintReferenceType<T>(T value) where T : class
=> Console.WriteLine(value is null ? "null" : value.ToString());
}
public sealed class Widget
{
public string Name { get; } = "Widget";
}