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) more times than I have trimmed my beard, and that is saying something. C# 14 finally removes that little papercut with support for unbound generic types in nameof, so we can stop sprinkling dummy types around like confetti at a dev conference.

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>)); // Logger

It 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<>)); // Logger
Console.WriteLine(nameof(DataMapper<,>)); // DataMapper

Yes, 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-JediCache
Console.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`2
Console.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.