Generics
Type parameters, constraints, and reusable generic types
Introduction
In this lesson, you'll learn about generics in C#. Coming from JavaScript, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In JavaScript, you're familiar with type parameters, constraints, and reusable generic types.
C# has its own approach to type parameters, constraints, and reusable generic types, which we'll explore step by step.
The C# Way
Let's see how C# handles this concept. Here's a typical example:
// Generic method
T Identity<T>(T value) => value;
// Generic class
public class Box<T>
{
public T Value { get; }
public Box(T value) => Value = value;
}
// Type constraints
T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) > 0 ? a : b;
// Multiple constraints
void Register<T>(T entity)
where T : class, IEntity, new()
{ /* T must be a ref type, implement IEntity, and have parameterless ctor */ }
// Generic interface
public interface IRepository<T> where T : class
{
T GetById(int id);
void Save(T entity);
}
// Usage
var box = new Box<string>("hello");
Console.WriteLine(box.Value); // "hello"
Max(3, 7); // 7Comparing to JavaScript
Here's how you might have written similar code in JavaScript:
// JS has no generics — everything is dynamic
function wrap(value) {
return { value };
}
function first(arr) {
return arr[0];
}
// No type safety
wrap("text"); // ok
wrap(42); // ok
first([1,2,3]); // 1You may be used to different syntax or behavior.
C# generics are reified — type info is preserved at runtime (unlike Java's erasure)
You may be used to different syntax or behavior.
'where T : class' constrains T to reference types; 'where T : struct' to value types
You may be used to different syntax or behavior.
'where T : new()' requires a parameterless constructor
You may be used to different syntax or behavior.
Multiple constraints: where T : IFoo, IBar, new()
You may be used to different syntax or behavior.
Covariance/contravariance via out/in keywords on generic interfaces
Step-by-Step Breakdown
1. Generic Methods
Append <T> after the method name. Type inference usually means you don't need to specify T explicitly.
function wrap(v) { return { value: v }; }T Wrap<T>(T v) => v;
// Caller: Wrap("text") — T inferred as string2. Generic Classes
Generic classes let you build type-safe containers. T is fixed at instantiation time.
class Stack { push(v) {...} pop() {...} }class Stack<T> {
List<T> _items = new();
public void Push(T item) => _items.Add(item);
public T Pop() { var v = _items[^1]; _items.RemoveAt(_items.Count-1); return v; }
}3. Type Constraints
Constraints let you call interface methods on T. Without constraints, only object members are available.
function max(a, b) { return a > b ? a : b; }T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;4. Variance (out/in)
out T = covariant (can return T but not accept); in T = contravariant (can accept T but not return). Used on interfaces only.
// No equivalent in JSinterface IProducer<out T> { T Produce(); }
interface IConsumer<in T> { void Consume(T item); }Common Mistakes
When coming from JavaScript, developers often make these mistakes:
- C# generics are reified — type info is preserved at runtime (unlike Java's erasure)
- 'where T : class' constrains T to reference types; 'where T : struct' to value types
- 'where T : new()' requires a parameterless constructor
Key Takeaways
- Generic methods: T Method<T>(T arg); classes: class Foo<T>
- Constraints (where) enable operations on T: IComparable, class, struct, new()
- C# generics are reified — typeof(T) works at runtime, unlike Java's erasure
- Use out/in on interface type parameters for covariance/contravariance