C# 14's Unbound Generics in nameof Explained
C# 14 adds support for unbound generic types in nameof, so you can write nameof(Logger<>) and nameof(Dictionary<,>) without supplying throwaway type arguments. This post explains the syntax, shows practical examples for logs, exceptions, and attributes, and clarifies how nameof differs from typeof in this context.
If you have ever passed a random type to get a clean name out of nameof, you are not alone. I have used nameof(Logger
The tiny problem that kept poking us
The nameof operator gives you refactor friendly strings. The catch has been generic types. Before C# 14, the compiler made you supply a type argument just to get the name of a generic definition.
Let me remind you how that felt.
using System;
public class Logger<T> {}public class User {}
Console.WriteLine(nameof(Logger<User>)); // LoggerIt works, but User is doing nothing here. It is like inviting Dwight to a meeting that could have been an email.
The C# 14 upgrade
C# 14 lets you pass an unbound generic to nameof. That means you ask for the definition, not a specific construction. Use empty brackets for one type parameter, or commas for more.
using System;
public class Logger<T> {}public class DataMapper<TSource, TDest> {}
Console.WriteLine(nameof(Logger<>)); // LoggerConsole.WriteLine(nameof(DataMapper<,>)); // DataMapperYes, those angle brackets are empty on purpose. For two parameters, the comma tells the compiler there are two slots.
- One parameter:
<> - Two parameters:
<,> - Three parameters:
<,,>
Why this matters in real code
1) Cleaner diagnostics and logs
When you build exception messages or logs, you want strings that survive refactors. nameof keeps you safe without dragging in a fake type.
using System;
public class HobbitBag<T> {}
throw new NotSupportedException( $"Use {nameof(HobbitBag<>)} for typed storage.");2) Attributes that stay honest
Attribute arguments must be constants. Since nameof is a constant expression, you can build attribute messages without concatenation gymnasts.
using System;
public class Repository<T> {}
const string Replacement = $"Use {nameof(Repository<>)} instead";
[Obsolete(Replacement)]public class OldRepo {}3) Refactor friendly metadata
Maybe you publish metrics, categories, or feature flags that include type names. Keep those names aligned with the code.
using System;
public class JediCache<T> {}
var metricName = $"hit-rate-{nameof(JediCache<>)}"; // hit-rate-JediCacheConsole.WriteLine(metricName);nameof vs typeof when generics are involved
A quick comparison helps set expectations.
using System;
Console.WriteLine(typeof(System.Collections.Generic.Dictionary<,>).Name); // Dictionary`2Console.WriteLine(nameof(System.Collections.Generic.Dictionary<,>)); // Dictionary- typeof gives you a runtime Type and its CLR style name with arity (
Dictionary2`). - nameof gives you the simple identifier, perfect for messages, attributes, and keys.
Limits to keep in mind
- This syntax works in nameof. It does not create or refer to a usable runtime type by itself.
- You get the simple name only. No namespace and no arity suffix.
- This targets generic type definitions. It is not for generic methods.
A tiny feature with outsized polish
Little language paper cuts add up. Removing the need for dummy type arguments in nameof cleans up messages, attributes, and logs, and it makes intent obvious. Fewer hacks, more clarity. My beard approves.
Quick recap
- Use
nameof(Foo<>)for single parameter generic types. - Use
nameof(Bar<,>)for two parameters and so on. - Prefer nameof over string literals for messages and attributes.
- Use typeof when you need a runtime Type, nameof when you need a stable identifier.
The next time you are tempted to toss in string or int just to make nameof happy, reach for the empty brackets. Your future self will nod knowingly, perhaps while sipping coffee and wondering where all the dummy types went.