Alias Any Type: Using Directives Beyond Namespaces

C# has supported using aliases since version 1.0, but they were limited to named types and namespaces. You could write using JsonDoc = System.Text.Json.JsonDocument; but you could not alias a tuple, an array, a pointer type, or a generic instantiation. C# 12 lifts this restriction — you can now alias any type.

The Syntax

The syntax uses the existing using alias directive, but now accepts any type on the right-hand side:

Example.cs
using Point = (double X, double Y);
using Matrix = double[,];
using Lookup = System.Collections.Generic.Dictionary<string, List<int>>;
using Handler = System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task>;

These aliases are file-scoped and can be used throughout the file as though they were regular type names.

Tuple Aliases

This is arguably the most impactful use case. Tuples are common in modern C#, but complex tuple signatures become unwieldy:

Example.cs
// Before
public (string Name, int Age, List<string> Roles) GetUserInfo(int userId) { ... }
public void ProcessUser((string Name, int Age, List<string> Roles) user) { ... }

// After
using UserInfo = (string Name, int Age, System.Collections.Generic.List<string> Roles);

public UserInfo GetUserInfo(int userId) { ... }
public void ProcessUser(UserInfo user) { ... }

The alias gives a meaningful name to the tuple shape without introducing a dedicated class or record. It is lightweight semantic typing.

Simplifying Complex Generics

Deeply nested generic types are a reality in many codebases, especially those using dictionaries of collections or functional types:

Example.cs
using RouteTable = Dictionary<string, List<(string Method, Delegate Handler)>>;
using Result = System.Threading.Tasks.ValueTask<(bool Success, string? Error)>;

public class Router
{
    private readonly RouteTable _routes = new();

    public async Result HandleRequest(string path, string method)
    {
        if (!_routes.TryGetValue(path, out var handlers))
            return (false, "Route not found");

        var match = handlers.FirstOrDefault(h => h.Method == method);
        if (match == default)
            return (false, "Method not allowed");

        await (Task)match.Handler.DynamicInvoke()!;
        return (true, null);
    }
}

Without the alias, every method signature involving RouteTable would need the full generic expansion.

Unsafe and Pointer Types

For developers working with interop or unsafe code, aliasing pointer and function pointer types is now possible:

Example.cs
using NativeBuffer = byte*;
using NativeCallback = delegate* unmanaged<int, void>;

This makes interop signatures significantly more readable.

Combining with Global Usings

Type aliases can be combined with the global modifier to make them available across an entire project:

GlobalUsings.cs
global using UserId = System.Guid;
global using Timestamp = System.DateTimeOffset;
global using ConnectionString = System.String;

This is a lightweight alternative to creating wrapper types when you want semantic type names without the overhead of defining new structs or classes. It does not provide type safety — a UserId is still a Guid and can be used interchangeably — but it communicates intent clearly.

Important Constraints

Aliases must use fully qualified type names. You cannot reference other using directives within an alias:

Example.cs
using Ns = MyApp.Models;
using User = Ns.User; // Error — cannot reference alias 'Ns'

// Must write the full path
using User = MyApp.Models.User; // Correct

Nullable reference type annotations are also not permitted in alias definitions:

Example.cs
using MaybeName = string?; // Error
using MaybeName = string;  // Fine — apply ? at usage sites

When It Helps

Alias any type is most useful in three scenarios. First, giving names to tuple types that appear in multiple method signatures. Second, taming deeply nested generics that make signatures hard to read. Third, creating project-wide semantic aliases via global usings to express domain concepts like UserId or Timestamp without creating dedicated types.

It is not a replacement for proper domain types when you need validation, behaviour, or type safety. But for the many cases where you just want a readable name for a complex type expression, it removes friction that previously pushed developers towards either verbose signatures or premature type creation.