Back to Blog
C#

C# Multiple Generic Type Parameters: Syntax and Usage

c# multiple generic type parameters: Learn how to declare and use C# generic types and methods with multiple type parameters, including constraints, type inference, an...

C# genericsgeneric typestype parameterstype inferencegeneric methods
Illustration of a C# generic class with two type parameter slots being filled by concrete type tokens, shown as interlocking geometric blocks.

c# multiple generic type parameters requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In C#, declaring multiple generic type parameters on a class, struct, interface, or method is a common requirement. The syntax is straightforward: list each parameter inside the angle brackets, separated by commas. A class such as Dictionary<TKey, TValue> is the most familiar example, but the same mechanism applies to any type or method you define yourself.

Declaring a Generic Type with Multiple Type Parameters

When a generic type needs more than one type argument, you declare each parameter inside the angle brackets, separated by commas:

public class Pair<TFirst, TSecond> { public TFirst First { get; set; } public TSecond Second { get; set; } public Pair(TFirst first, TSecond second) { First = first; Second = second; } }

The order of the type parameters matters. Pair<string, int> is a different closed generic type from Pair<int, string>. Callers must supply type arguments in the declared order, and the compiler enforces that at every usage site.

You can instantiate the type with any combination of type arguments:

var nameAndAge = new Pair<string, int>("Alice", 34); var coordinate = new Pair<double, double>(12.5, -7.25);

The same class definition serves both cases because the type parameters are placeholders that are replaced when the type is closed at compile time.

Multiple Type Parameters in Generic Methods

Methods can also declare multiple type parameters independently of their containing type:

public static TResult ConvertValue<TInput, TResult>(TInput input) { return (TResult)Convert.ChangeType(input, typeof(TResult)); }

When the method is called, the compiler attempts to infer the type arguments from the arguments passed. If every type parameter appears in at least one method parameter, inference usually works:

public static void PrintPair<TFirst, TSecond>(TFirst first, TSecond second) { Console.WriteLine($"{first}: {second}"); }

Both type parameters can be inferred from the arguments, so the call needs no explicit type arguments:

PrintPair("key", 42); // TFirst is string, TSecond is int

But when a type parameter appears only in the return type, as TResult does in ConvertValue, callers must supply it explicitly:

int number = ConvertValue<string, int>("42");

Applying Constraints to Each Type Parameter Independently

Each type parameter can carry its own constraint clause. The where clauses appear after the class or method signature, one per type parameter:

public class Repository<TEntity, TKey> where TEntity : class, new() where TKey : struct { public TKey Id { get; set; } public TEntity Entity { get; set; } }

The first constraint requires TEntity to be a reference type with a parameterless constructor. The second requires TKey to be a value type. The compiler checks both at every usage site, so Repository<Customer, int> is valid while Repository<Customer, string> fails because string is not a value type.

Constraints apply independently, which means you can mix different kinds of restrictions. This is useful when one type parameter represents an entity and another represents a key, an identifier, or a configuration value.

Common Framework Types That Use Multiple Type Parameters

Several BCL types rely on multiple type parameters, and their usage patterns are worth recognizing:

TypeType ParametersTypical Use
Dictionary<TKey, TValue>key and valueKeyed lookup
KeyValuePair<TKey, TValue>key and valueRepresenting one entry
Tuple<T1, T2>two valuesLightweight grouping
Func<T1, T2, TResult>two inputs and a resultDelegate with two arguments
Action<T1, T2>two inputsDelegate returning void

Dictionary<TKey, TValue> is the most common example. The key type is used for hashing and comparison, while the value type is stored alongside it. The two type parameters play different roles, which is why the framework keeps them separate rather than forcing a single type.

Type Inference with Multiple Type Parameters

The compiler infers each type parameter independently from the arguments supplied. With multiple parameters, inference can fail when one parameter is not represented in the argument list.

Consider this method:

public static TTarget Map<TSource, TTarget>(TSource source) { // mapping logic }

TSource can be inferred from the argument, but TTarget cannot, because it appears only in the return type. The call must specify both:

var result = Map<Customer, CustomerDto>(customer);

When inference fails, the compiler reports CS0411: "The type arguments for method cannot be inferred from the usage. Try specifying the type arguments explicitly." The fix is to supply the missing type arguments explicitly, in the declared order.

Runtime Behavior and Code Specialization

Generic types with multiple type parameters are closed at runtime. Each distinct combination of type arguments produces a distinct closed generic type. For value type arguments, the runtime generates specialized code; for reference type arguments, the runtime shares the same code because references have a uniform representation.

This means Dictionary<int, string> and Dictionary<long, string> are separate closed types with their own method tables, while Dictionary<string, int> and Dictionary<object, int> share the reference-type code path. The practical consequence is that there is no single shared static field across all instantiations of a generic type with multiple parameters. A static field in Pair<TFirst, TSecond> exists separately for Pair<int, string> and Pair<string, int>.

Common Mistakes with Multiple Type Parameters

The most frequent error is reversing the order of type arguments. Dictionary<int, string> maps integers to strings; Dictionary<string, int> maps strings to integers. The compiler does not catch the mistake when both types are valid, so the error surfaces only when the code misbehaves at runtime.

Another common mistake is applying a constraint to the wrong type parameter. Each where clause must name the parameter it constrains, and the compiler enforces the match. A constraint written for TKey does not apply to TEntity.

A third issue is forgetting that type inference stops at the first unresolved parameter. When one type parameter cannot be inferred, the compiler does not attempt to infer the others; it reports an error and expects explicit type arguments.

c# multiple generic type parameters: Practical Usage and Cod | RYUSLOG DEV