Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close
    Cheat sheets: all cheat sheets

    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

    C#
    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.

    Text
    42
    omega
    Widget
    hello
    Widget
    constraints-ok
    • 42 wins the integer comparison because it is greater than 17.
    • omega follows alpha under the stated culture. string.CompareTo uses the current culture and is case-sensitive; this helper does not promise ordinal string ordering. See String.CompareTo.
    • The first Widget is the value of widget.Name, initialized by the property declaration while CreateDefault<Widget>() constructs the object.
    • hello comes from printing the string instance.
    • The second Widget is the object's type name: this class does not override ToString() and is declared without a namespace. It is not a read of Name. See Object.ToString.
    • constraints-ok is the final literal printed by Main; 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 where clause per constrained type parameter.
    • At most one of class, class?, struct, notnull, and unmanaged can appear; put it first.
    • A base-class constraint precedes interface or type-parameter constraints.
    • new() follows the other constraints. An applicable allows ref struct anti-constraint follows even new().
    • Do not combine new() with struct or unmanaged; do not combine struct with unmanaged.
    • 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 create T
    • Assuming struct and unmanaged are 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 notnull violations 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

    1. Why does a generic method need a constraint before it can call a method on T?
    2. When would you choose an interface constraint over a base-class constraint?
    3. What capability does new() add, and why does it not work with every type?
    4. How is notnull different from class?
    5. Why can constraints improve both correctness and API design?

    Answer key

    1. The generic body must be valid for every allowed T. In this program, IComparable<T> supplies the CompareTo member that Max uses. Unconstrained T still supports object-level operations; it does not support every member found on a particular caller's type.
    2. Choose an interface when the algorithm needs a capability shared by otherwise unrelated types. Max needs comparison, so requiring all inputs to inherit from one application base class would unnecessarily narrow its callers.
    3. new() permits new T(). For a class argument, the class must be non-abstract and expose a public parameterless constructor. Widget qualifies; string does not. A type requiring constructor arguments needs another creation strategy, such as a supplied factory. See the constructor constraint.
    4. In nullable-enabled code, class accepts non-nullable reference types; notnull additionally accepts non-nullable value types such as int. class? is the reference-type constraint to use when nullable type arguments are intended.
    5. Constraints make the implementation's dependencies visible and checkable at call sites. For example, Max(new Widget(), new Widget()) is rejected because this Widget does not implement IComparable<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, and new() 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 ·