How to Work with Enum Underlying Types in C

By default, every enumeration in C# uses the 32-bit signed integer (int) as its underlying storage mechanism. However, C# allows developers to override this default and choose from a specific set of integral types—such as byte, short, or long—to optimize memory allocation, maintain strict API contracts, or integrate cleanly with databases and network protocols. This guide covers how to declare an enum with a custom underlying type, safely cast between the enum and its primitive value, and retrieve underlying type information at runtime.

Supported Underlying Types

An enum in C# can inherit from any integral type except char. The allowed types are:

  • byte and sbyte (8-bit)
  • short and ushort (16-bit)
  • int and uint (32-bit)
  • long and ulong (64-bit)

Declaring an Enum with a Specific Underlying Type

To specify an underlying type, place a colon (:) followed by the chosen type name after the enum identifier:

public enum NetworkState : byte
{
    Disconnected = 0,
    Connecting = 1,
    Connected = 2,
    Error = 255
}

In this example, each member of NetworkState requires only 1 byte of memory instead of the default 4 bytes. If an assigned value exceeds the range of the underlying type, the C# compiler produces a compile-time error.

Casting Between Enums and Underlying Types

C# requires explicit casting to convert an enum instance to or from its underlying type:

// Enum to underlying type
NetworkState state = NetworkState.Connected;
byte rawValue = (byte)state; // rawValue is 2

// Underlying type to enum
byte input = 1;
NetworkState restoredState = (NetworkState)input; // restoredState is NetworkState.Connecting

While casting to an integral type of the same size or larger is safe, casting directly to an incompatible integral type can cause truncation or runtime errors if done improperly. Always match the explicit cast to the defined underlying type or a larger type.

Inspecting Underlying Types at Runtime

To determine the underlying storage type dynamically, use the Enum.GetUnderlyingType method:

Type underlyingType = Enum.GetUnderlyingType(typeof(NetworkState));
Console.WriteLine(underlyingType); // Outputs: System.Byte

This is particularly useful when writing generic serialization logic, database mapping layers, or reflection-based validators where the enum structure must be inspected programmatically.

Using Underlying Types with the [Flags] Attribute

When using the [Flags] attribute for bitwise operations, choosing a larger underlying type like long expands the maximum number of distinct bit flags from 32 to 64:

[Flags]
public enum UserPermissions : ulong
{
    None = 0,
    Read = 1UL << 0,
    Write = 1UL << 1,
    Execute = 1UL << 2,
    // Can safely scale up to 1UL << 63
}

Choosing the correct underlying type ensures efficient memory usage in high-throughput applications, prevents overflow errors in bitmask scenarios, and enforces correct data representations across system boundaries.