Generic Constraints in C#: Design Contracts for Type Parameters
Read common generic constraints and the capabilities they allow.
Start with the calls
Which capability lets Max(42, 17) compare values, and which lets CreateDefault<Widget>() construct one? Predict the two contracts before reading the constraint rules. Nullability annotations remain compile-time analysis; they do not add runtime guards.
Executable code examples
Generic constraints in action
Setup
Use a .NET 8 or later console project. Run dotnet new console -n GenericConstraintsDemo, replace the entire generated Program.cs with the code below, then run dotnet run --project GenericConstraintsDemo. Keep <Nullable>enable</Nullable> in the project file when exploring the nullable-contract discussion. No extra packages are needed. The allows ref struct ordering note describes newer C# syntax; the runnable program does not use it.
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";
}Expected output and explanation
This output assumes an en-US current culture for the string comparison.
42 omega Widget hello Widget constraints-ok
42wins the integer comparison because it is greater than17.omegafollowsalphaunder the stated culture.string.CompareTouses the current culture and is case-sensitive; this helper does not promise ordinal string ordering. See String.CompareTo.- The first
Widgetis the value ofwidget.Name, initialized by the property declaration whileCreateDefault<Widget>()constructs the object. hellocomes from printing the string instance.- The second
Widgetis the object's type name: this class does not overrideToString()and is declared without a namespace. It is not a read ofName. See Object.ToString. constraints-okis the final literal printed byMain; it is a completion marker, not an automated assertion that every constraint rule was tested.
Try these predictions separately before changing the main example: PrintReferenceType(42) is rejected because int violates class; CreateDefault<string>() is rejected because string has no public parameterless constructor.
Deep dive
A generic constraint is declared with a where clause. On a generic method, the clause follows the method parameter list; on a type, it follows any base-class and interface list. The constraint describes the permitted type arguments. 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
A few rules matter in practice:
- Use one
whereclause per constrained type parameter. - At most one of
class,class?,struct,notnull, andunmanagedcan appear; put it first. - A base-class constraint precedes interface or type-parameter constraints.
new()follows the other constraints. An applicableallows ref structanti-constraint follows evennew().- Do not combine
new()withstructorunmanaged; do not combinestructwithunmanaged. - Constraints do not add argument-value checks.
For example, where T : class, IComparable<T>, new() puts the reference-type requirement before the interface, then the constructor requirement. See the constraint grammar.
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 confusing a type contract with a value check. With nullable analysis enabled, class requires a non-nullable reference type; class? also permits nullable reference types. notnull permits non-nullable reference types and non-nullable value types. Neither inserts a runtime null guard. See the where reference and nullable reference types.
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 be ordinary constraint errors: they produce nullable warnings when nullable analysis is enabled; treating warnings as errors can then fail the build
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?
Answer key
- The generic body must be valid for every allowed
T. In this program,IComparable<T>supplies theCompareTomember thatMaxuses. UnconstrainedTstill supports object-level operations; it does not support every member found on a particular caller's type. - Choose an interface when the algorithm needs a capability shared by otherwise unrelated types.
Maxneeds comparison, so requiring all inputs to inherit from one application base class would unnecessarily narrow its callers. new()permitsnew T(). For a class argument, the class must be non-abstract and expose a public parameterless constructor.Widgetqualifies;stringdoes not. A type requiring constructor arguments needs another creation strategy, such as a supplied factory. See the constructor constraint.- In nullable-enabled code,
classaccepts non-nullable reference types;notnulladditionally accepts non-nullable value types such asint.class?is the reference-type constraint to use when nullable type arguments are intended. - Constraints make the implementation's dependencies visible and checkable at call sites. For example,
Max(new Widget(), new Widget())is rejected because thisWidgetdoes not implementIComparable<Widget>. Keeping only the capabilities the body uses also preserves more valid callers.
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 can distinguish compiler constraint checks and nullable warnings from runtime argument validation.
- 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.
Worked example
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 above 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. With no other constraint on this factory, new() supplies the construction capability.
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. The explicit null branch handles null at runtime. Declare the parameter as T? if null is intended.
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.
TechInterviewNotes Editorial Team · Updated 28 Oct 2026 ·