Union type
In computer science, a union type is a data type that represents the union of two or more types, allowing a value to belong to any one of those types at a time, often interpreted set-theoretically as the set of all values from the constituent types.[1] This concept enables variables or expressions to flexibly accommodate multiple possible data representations, contrasting with product types like records or structs that combine multiple values simultaneously.[1]
Practical implementations of union types first appeared in programming languages in the 1970s, such as variant records in Pascal and untagged unions in C.[2] In practice, they manifest in two primary forms: untagged unions, which overlap memory for different types to optimize storage, and tagged unions (also known as sum types or variants), which include a discriminator to ensure type safety at runtime or compile time.[2] Untagged unions, as in the C programming language, allocate space equal to the largest member and allow only one member to be active, making them useful for memory-efficient type punning but prone to undefined behavior if misused.[2] For example, a C union might store either an integer or a floating-point number in the same memory location, with the size determined by the larger type.[2]
In modern statically typed languages, union types emphasize safety and expressiveness through tagging and type narrowing. In TypeScript, unions are denoted with the | operator, such as string | number, permitting functions to accept values of either type while restricting access to shared properties.[3] Languages like Rust implement safe tagged unions via enums, which support pattern matching for exhaustive handling of variants, as in defining a Result<T, E> type that can hold either a success value or an error. Similarly, in functional languages like OCaml or Haskell, algebraic data types provide union-like sum types for modeling disjoint alternatives, such as shapes in a geometry system (e.g., circle or rectangle). These features support advanced type inference, occurrence typing, and function overloading, enhancing code reliability in applications from web development to systems programming.[1]
Overview
Definition and purpose
A union type is a user-defined data type in programming languages that enables a single variable to store values of different types, with all possible members occupying the same contiguous block of memory and only one member holding a valid value at any given time.[4][5] This overlapping storage contrasts with other composite types, as the size of a union is determined solely by the largest member, allowing efficient reuse of space for mutually exclusive data variants.[5]
The primary purpose of union types is to optimize memory usage by permitting the representation of variant data—such as a field that might hold an integer or a string—without allocating separate space for each possibility, which is particularly valuable in systems programming or embedded environments where resources are limited.[5] Additionally, unions facilitate type punning, a technique for reinterpreting the binary representation of data as a different type to perform low-level manipulations, such as byte-order conversions or hardware register access. In certain contexts, they support polymorphism-like behavior by enabling the storage of disparate object types within a uniform structure, aiding in the design of flexible data representations.
Unlike structs, which provide non-overlapping storage for all members and thus consume memory proportional to the sum of their sizes, unions enforce shared allocation to promote compactness.[6] Enumerations, by comparison, serve to define named integer constants without inherent data storage for variants, focusing instead on symbolic representation rather than polymorphic containment.[7] Union types may be implemented as untagged variants, depending on external context to identify the active member, or tagged variants, incorporating a type discriminator for runtime safety.[8]
Overall, the key benefits of union types lie in their ability to deliver space efficiency and representational flexibility, allowing developers to model data that inherently varies in form while minimizing overhead.[5]
Historical development
Early implementations of union types appeared in PL/I, released in 1964, where they were defined using the UNION keyword to overlay storage for different data elements.[9] The concept was further formalized in the ALGOL 68 programming language, introduced in 1968, where they were defined as "united modes" to allow variables to hold values of multiple types while incorporating modes for runtime type safety and introspection.[10] This design emphasized expressive type systems, enabling programmers to declare unions that could dynamically determine and enforce the active type, influencing subsequent languages' approaches to type flexibility.[11]
In the early 1970s, union types evolved in systems-oriented languages for memory efficiency. Pascal, released in 1970, introduced tagged unions through variant records, which used a discriminant tag to safely distinguish among alternative types within a record, providing compile-time checks against invalid access.[12] Concurrently, C, developed by Dennis Ritchie between 1971 and 1973, adopted untagged unions to facilitate low-level memory manipulation and data overlaying without runtime overhead, prioritizing portability and performance in Unix system programming.[13] These developments in Pascal and C highlighted a divergence: tagged variants for safety versus untagged for raw control.
The influence of these early unions extended to variant types in functional programming, where sum types—essentially tagged unions—emerged in languages like ML (1973) and later Haskell (1990) to model disjoint choices with exhaustive pattern matching for error-free handling.[14] In modern dynamic and typed languages, union types have expanded for better expressiveness in type hinting and structural typing. PHP 8.0 (2020) added native union types for parameters, properties, and returns, allowing multiple type declarations separated by pipes.[15] TypeScript introduced union types in version 1.4 (2015), enabling values to be one of several types via the | operator for safer JavaScript development.[16] Python 3.10 (2021) simplified unions with the | syntax for annotations, reducing reliance on the typing.Union class.[17]
Meanwhile, Go has seen experimental proposals for restricted union types using type sets in interfaces, aiming to address limitations in handling variant data without full sum type adoption as of 2025.[18]
Classification
Untagged unions
Untagged unions, also known as untagged or discriminated-free unions, are a form of union type in programming languages that lack a runtime discriminant or tag to specify which member of the union is currently active, placing the responsibility on the programmer to maintain awareness of the active type.[19] This design allows all possible members to overlay the same block of memory, enabling efficient space usage where only one variant is needed at a time, but it requires explicit tracking to avoid misinterpretation of the stored value.[20]
A key characteristic of untagged unions is their shared memory allocation, where the size of the union equals the size of its largest member, and assigning to one member overwrites the data of others, potentially leading to undefined behavior if the programmer accesses an incorrect member. For instance, in generic pseudocode, an untagged union might be declared as:
union Example {
int integer;
float real;
};
union Example {
int integer;
float real;
};
Here, setting u.integer = 1 would store the integer value in the shared memory, but reading u.real could produce garbage or erroneous results unless the programmer ensures the type matches the intended use.[20]
Untagged unions find common application in embedded systems and performance-critical code, particularly for type punning, where raw bytes are reinterpreted as different types to optimize access to hardware registers or packed data structures without additional overhead.[21] For example, they enable efficient manipulation of bitfields in low-level device drivers by allowing direct overlay of scalar types onto structured representations.[22]
Historically, untagged unions have been a staple in C-family languages since the development of C in the early 1970s by Dennis Ritchie at Bell Labs, where they provided a lightweight mechanism for variant data handling in systems programming without the complexity of type tags. In contrast to tagged unions, which incorporate a discriminant for runtime type identification to enhance safety, untagged variants prioritize raw efficiency at the cost of potential errors.[19]
Tagged unions
Tagged unions, also known as discriminated unions or sum types, pair a union data structure with an explicit tag or enumerator field that indicates which member of the union is valid at runtime. This tag serves as a discriminator, allowing the program to perform type checking before accessing the underlying data, thereby mitigating risks associated with incorrect variant interpretation.[23][24]
These structures are particularly prevalent in functional programming paradigms, where they form the basis of algebraic data types by representing a finite set of mutually exclusive possibilities, each potentially carrying associated data. The tag ensures that only the appropriate variant is accessed, enforcing type safety either at compile time in statically checked languages or via runtime verification. Unlike untagged unions, which lack this mechanism and depend on manual tracking, tagged unions systematically prevent invalid operations, reducing errors in variant handling.[25][26]
Common use cases include modeling optional or nullable values through types like Maybe or Option, which encapsulate either a present value or an absence indicator, and error-handling constructs such as Result or Either, distinguishing between successful outcomes and failure states with descriptive payloads. These applications promote robust code by making invalid states unrepresentable and facilitating exhaustive pattern matching over all variants.[27]
For illustration, consider a pseudocode representation of a simple tagged union for holding either an integer or a string:
enum VariantType { [INTEGER](/page/Integer), [STRING](/page/STRING) };
struct TaggedValue {
[VariantType](/page/The_Variant) tag;
union {
[int](/page/INT) integerValue;
[char*](/page/Char) stringValue;
} [payload](/page/Payload);
};
enum VariantType { [INTEGER](/page/Integer), [STRING](/page/STRING) };
struct TaggedValue {
[VariantType](/page/The_Variant) tag;
union {
[int](/page/INT) integerValue;
[char*](/page/Char) stringValue;
} [payload](/page/Payload);
};
To initialize an integer variant:
TaggedValue tv;
tv.tag = [INTEGER](/page/Integer);
tv.payload.integerValue = 42;
TaggedValue tv;
tv.tag = [INTEGER](/page/Integer);
tv.payload.integerValue = 42;
Access requires tag verification:
if (tv.tag == [INTEGER](/page/Integer)) {
// Safely use tv.payload.integerValue
} else {
// Handle mismatch, e.g., raise error or default
}
if (tv.tag == [INTEGER](/page/Integer)) {
// Safely use tv.payload.integerValue
} else {
// Handle mismatch, e.g., raise error or default
}
This pattern guarantees that the payload is interpreted correctly, with the tag acting as a runtime sentinel.[28]
Technical details
Memory representation
In union types, all members occupy the same memory region, with their storage overlapping such that each member begins at the initial offset of the union object, enabling only one member to hold a valid value at any time without individual allocations.[20]
The size of a union is determined by the size of its largest member, rounded upward as necessary to meet the alignment constraints of that member or the overall structure. For instance, a union containing a 4-byte integer and an 8-byte double would have a size of 8 bytes to accommodate the larger type.[20]
The alignment requirement of the union matches that of its most strictly aligned member, ensuring the entire object can be placed at valid addresses; additional padding bytes may be inserted at the end to fulfill this, particularly for array compatibility.[20]
Unions facilitate type punning by allowing the bit pattern stored in one member to be reinterpreted as another member's type when accessed, a technique that is explicitly permitted in C to adhere to strict aliasing rules without invoking undefined behavior.
Type safety considerations
Union types, particularly untagged variants, pose significant type safety risks due to the potential for accessing an inactive member, which results in undefined behavior. In languages like C and C++, reading from a union member that was not the most recently written to can lead to misinterpretation of the shared memory, causing bit-level misalignment, data corruption, or program crashes.[29] This occurs because all members overlay the same memory region, and without an indicator of the active type, the compiler cannot enforce correct access, allowing erroneous code to compile and execute unpredictably.[29]
Tagged unions mitigate these issues by including a discriminator (such as an enum) to identify the active variant, but safety is not inherent; if the tag is not checked before accessing the data, tag mismatches can still trigger undefined behavior akin to untagged cases.[29] For instance, assuming the wrong variant is active may lead to invalid memory reads or writes, exacerbating the risks of misalignment or invalid operations. To address this, developers must explicitly validate the tag prior to access, ensuring the correct member is used.[29]
Mitigation strategies include leveraging compiler extensions and tools to enforce stricter rules. In GCC, the -fstrict-aliasing flag enables optimizations based on the strict aliasing rule, which assumes pointers of different types do not alias the same object; however, improper union use for type punning can violate this, so disabling it with -fno-strict-aliasing is recommended when necessary to avoid optimization-induced bugs.[30] Additionally, static analysis tools can detect potential misuse by tracking active members and flagging unchecked accesses.[29]
Best practices for safe union usage emphasize proactive measures: always initialize and maintain the tag in tagged unions to track the active variant; avoid unions for types with non-overlapping memory layouts or non-trivial constructors/destructors, as these complicate lifetime management; and prefer type-safe alternatives like C++'s std::variant, which tracks the active type at runtime and throws an exception on invalid access, preventing undefined behavior.[31] In modern languages such as Rust, enums serve as tagged unions with compile-time enforcement of variant handling via pattern matching, providing memory safety without runtime checks through zero-cost abstractions that compile to efficient machine code.[32]
While tags introduce performance trade-offs, such as additional memory overhead (typically 1-8 bytes for the discriminator depending on the enum size), they enable compiler optimizations like improved branch prediction in switch statements on the tag, potentially reducing execution time in hot paths compared to unchecked untagged access.[29] This balance favors tagged approaches in safety-critical code, where the modest space cost outweighs the risks of undefined behavior.
Language implementations
C and C++
In C, unions are declared using the syntax union name { member-declarations };, where the members are a sequence of declarations sharing the same memory location, and the size of the union is determined by the largest member, potentially with padding for alignment. The union is untagged by default, meaning there is no built-in mechanism to track which member is active, requiring manual management by the programmer. Unions in C are typically used for space-efficient storage of variant data or for type punning, where the shared memory allows reinterpretation of the same bytes as different types.[33]
A common example is a union to hold either an integer or a character array, as shown below:
c
union Data {
[int](/page/INT) i;
char str[10];
};
int main() {
union Data d;
d.i = 42; // Initialize with [int](/page/INT)
// Now access as char array ([type punning](/page/Type_punning))
for ([int](/page/INT) j = 0; j < 10; ++j) {
printf("%c", d.str[j]);
}
return 0;
}
union Data {
[int](/page/INT) i;
char str[10];
};
int main() {
union Data d;
d.i = 42; // Initialize with [int](/page/INT)
// Now access as char array ([type punning](/page/Type_punning))
for ([int](/page/INT) j = 0; j < 10; ++j) {
printf("%c", d.str[j]);
}
return 0;
}
This demonstrates type punning, which is permitted in C when accessing through the union type, allowing safe reinterpretation of the integer's bytes as characters.[34] Another practical use is detecting endianness via a union with an integer and byte array:
c
union Endian {
int value;
unsigned char bytes[sizeof(int)];
};
int main() {
union Endian e;
e.value = 1;
if (e.bytes[0] == 1) {
// Little-endian
} else {
// Big-endian
}
return 0;
}
union Endian {
int value;
unsigned char bytes[sizeof(int)];
};
int main() {
union Endian e;
e.value = 1;
if (e.bytes[0] == 1) {
// Little-endian
} else {
// Big-endian
}
return 0;
}
Such patterns rely on the union's overlapping storage but must adhere to strict aliasing rules, which prohibit direct pointer aliasing outside the union context to enable compiler optimizations.[35]
C++ inherits C's union syntax and semantics but introduces restrictions and extensions. Unions in C++ cannot have virtual functions, base classes, or be used as bases, and they support member functions including constructors and destructors under certain conditions. C++11 expanded support to allow non-POD (Plain Old Data) types in unions, provided at most one member has a non-trivial constructor or destructor, and exactly one such member is initialized in a constructor. Anonymous unions, standard in C++ for embedding members directly into enclosing structs without qualification (e.g., struct S { union { int a; float b; }; }; accesses s.a), are a GCC extension in C via unnamed fields.[36] GCC also provides transparent unions as an extension, treating function arguments of union type as compatible with any member type for overloading.
A key pitfall in both C and C++ is undefined behavior from violating strict aliasing, where accessing a union member via a pointer of an incompatible type (outside the union) can lead to optimization errors or crashes. Multi-threaded access to unions without synchronization risks data races, as the active member is not atomically tracked, potentially causing inconsistent reads. As a safer alternative in modern C++, std::variant (introduced in C++17) provides a type-safe union with compile-time type checking and runtime active-alternative tracking.
Rust
In Rust, unions provide a way to define untagged union types similar to those in C, where multiple fields overlap in the same memory location. The syntax for declaring a union mirrors that of a struct but uses the union keyword: union Name { field1: Type1, field2: Type2 }. For example:
rust
union MyUnion {
f1: u32,
f2: f32,
}
union MyUnion {
f1: u32,
f2: f32,
}
All fields share the same storage, so writing to one overwrites the others, and reading from a field requires an unsafe block to prevent undefined behavior from accessing invalid data. Initialization is safe, as in let u = MyUnion { f1: 1 };, but accessing fields demands explicit unsafe code, such as unsafe { let val = u.f1; }. This design ensures the borrow checker cannot verify memory safety automatically, placing responsibility on the programmer to track the active field. Unions have been stable since Rust 1.19.0 and remain unchanged in the 2021 edition, prioritizing explicit unsafety to avoid C-style runtime risks.[37][38]
For safe handling of variant data, Rust favors enums as tagged unions, which embed a discriminant tag to distinguish variants at compile time. The syntax is enum Name { Variant1(Type1), Variant2(Type2) }, where each variant can carry associated data. A canonical example is the standard Option<T> enum:
rust
enum Option<T> {
None,
Some(T),
}
enum Option<T> {
None,
Some(T),
}
Usage involves safe pattern matching with match, which the compiler enforces for exhaustiveness: all variants must be handled, or a default arm is required. For instance:
rust
let some_value = Some(42);
match some_value {
Some(x) => println!("Got {}", x),
None => println!("Nothing"),
}
let some_value = Some(42);
match some_value {
Some(x) => println!("Got {}", x),
None => println!("Nothing"),
}
This prevents invalid access, as the borrow checker ensures only the active variant's data is borrowed. Enums implement zero-cost tagged unions, with no runtime overhead—the tag and payload are stored contiguously, matching the size of the largest variant plus the tag. This approach contrasts with raw unions by integrating safety into the type system, eliminating the need for unsafe blocks in typical use.[32][39]
Python
In Python, union types are supported through the typing module for static type hinting, allowing variables, function parameters, and return values to be annotated as compatible with multiple types. Introduced in Python 3.5 via PEP 484, the Union type constructor enables annotations like Union[int, str], which indicate that a value can be of any of the specified types during static analysis.[40] These hints are optional and have no effect on runtime behavior, serving primarily to improve code documentation and enable tools for early error detection.[40]
Starting with Python 3.10, PEP 604 simplified union syntax by overloading the bitwise OR operator (|) for types, allowing int | str as a direct alternative to Union[int, str] in annotations and even in isinstance and issubclass calls.[41] This change enhances readability without requiring imports from the typing module for basic cases. For example, a function can be annotated as follows:
python
from typing import Union # For Python < 3.10
def process_input(value: Union[int, str]) -> None:
pass
# In Python 3.10+, equivalent to:
def process_input(value: int | str) -> None:
pass
from typing import Union # For Python < 3.10
def process_input(value: Union[int, str]) -> None:
pass
# In Python 3.10+, equivalent to:
def process_input(value: int | str) -> None:
pass
Such annotations help static type checkers infer compatibility, for instance, ensuring that process_input(42) or process_input("hello") are valid while rejecting incompatible types like process_input([1, 2]).[40][41]
Python's union types lack runtime enforcement or shared memory representation, distinguishing them from true unions in languages like C; instead, they are purely for static checking with tools such as mypy, which verifies annotations against code usage.[40] At runtime, union-like behavior can be emulated using isinstance checks or by defining classes and dataclasses that wrap variants, akin to tagged unions, to enforce type-specific logic. For instance:
python
from dataclasses import dataclass
from [typing](/page/Typing) import [Union](/page/Union)
@dataclass
class IntWrapper:
value: [int](/page/INT)
@dataclass
class StrWrapper:
value: [str](/page/€STR)
# Emulate union at runtime
def handle_variant(variant: [Union](/page/Union)[IntWrapper, StrWrapper]) -> None:
if isinstance(variant, IntWrapper):
print(f"Integer: {variant.value}")
elif isinstance(variant, StrWrapper):
print(f"String: {variant.value}")
from dataclasses import dataclass
from [typing](/page/Typing) import [Union](/page/Union)
@dataclass
class IntWrapper:
value: [int](/page/INT)
@dataclass
class StrWrapper:
value: [str](/page/€STR)
# Emulate union at runtime
def handle_variant(variant: [Union](/page/Union)[IntWrapper, StrWrapper]) -> None:
if isinstance(variant, IntWrapper):
print(f"Integer: {variant.value}")
elif isinstance(variant, StrWrapper):
print(f"String: {variant.value}")
This approach allows runtime discrimination but requires explicit implementation, as Python's dynamic nature does not natively support discriminated unions.[40]
Historically, before explicit unions, developers relied on the Any type from the typing module for flexible annotations, but unions marked a shift toward more precise static typing, reducing over-reliance on Any and improving checker accuracy in large codebases.[40] The 3.10 syntax further streamlined adoption, aligning Python's type system more closely with modern idioms while preserving backward compatibility.[41]
TypeScript
In TypeScript, union types enable a value to be one of several possible types, providing structural typing that enhances type safety in JavaScript development. This feature allows developers to model scenarios where a variable or property can hold values of multiple types, such as strings or numbers, without runtime enforcement since TypeScript is a compile-time superset of JavaScript.[3]
The syntax for declaring a union type uses the vertical bar (|) to separate constituent types, either as a named type alias or inline. For example, type ID = string | number; defines a type alias for an identifier that can be either a string or a number, while inline usage appears as function log(id: string | number) { ... }. Unions can combine primitive types, object types, or other unions, but access to properties is limited to those common across all members to prevent type errors.[3][42]
Type narrowing refines the inferred type within conditional blocks using type guards, such as typeof checks or in operators, allowing safe access to type-specific members. For instance, in a function handling a string | number parameter, an if (typeof value === "string") block narrows value to string, enabling methods like toUpperCase(). Discriminated unions extend this by using a shared literal-type discriminant property for precise narrowing via switches or if-statements. A common pattern involves objects with a kind or type field:
typescript
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
}
}
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
}
}
This ensures exhaustiveness checking when paired with the never type in a default case, catching unhandled variants at compile time.[3]
Unions integrate seamlessly with interfaces and function overloads; for example, an interface might declare a property as status: "loading" | "success" | "error", restricting values to those literals and improving autocomplete in editors like VS Code via enhanced IntelliSense. In function signatures, unions support overloads by specifying return types based on input narrowing, such as handling mixed arrays. Since unions are erased during compilation to plain JavaScript, there is no runtime overhead, but JavaScript's dynamic nature allows emulation through arrays or objects.[3][42]
Introduced in TypeScript 3.4, const assertions (as const) enhance unions by preserving literal types in expressions, preventing widening to broader types like string and enabling precise union construction from constants. For example, const directions = ["north", "east"] as const; infers type Directions = "north" | "east";, useful for creating closed literal unions without manual enumeration. Subsequent versions, including 4.0, improved related inference for variadic tuples and unions, facilitating more robust type combinations in advanced scenarios.[43][44]
PHP
Union types in PHP, introduced in version 8.0, enable developers to specify multiple acceptable types for function parameters, class properties, and return values, enhancing type safety in this dynamically typed language commonly used for web development.[45] This feature addresses limitations in PHP's prior type system by allowing declarations like int|string, where the vertical bar (|) separates individual types.[46] The mixed type, also added in PHP 8.0, serves as a wildcard encompassing any type, including unions, and can be used in these declarations to indicate broad compatibility.[47]
Union types are supported in function parameters, class properties, and return types, but PHP does not permit standalone union type declarations for local variables.[45] For instance, a class property can be declared as public [int](/page/INT)|[float](/page/Float) $value;, allowing the property to hold either an integer or a float value.[46] Similarly, functions can specify union types in parameters and returns, such as function processData([mixed](/page/mixed)|[array](/page/Array) $input): [string](/page/String)|[int](/page/INT) { return gettype($input) === 'array' ? 'processed' : 42; }, where type checking might use built-in functions like gettype() for runtime validation.[45] These declarations promote clearer code intent without requiring extensive documentation annotations.
Type enforcement for union types occurs primarily at runtime, throwing a TypeError if a value does not match any permitted type in the declaration, though this is partial due to PHP's inherent weak typing system.[45] Developers can enable stricter checking via declare(strict_types=1); at the file level, but backward compatibility with PHP's loose type coercion is maintained to avoid breaking existing codebases.[45] Limitations include prohibitions on combining singleton types like false and true in a union (use bool instead) and excluding void or never from unions.[45]
In PHP 8.1 and later, union types integrate with readonly properties, which can only be initialized once (typically in the constructor) and cannot be modified afterward, providing immutable typed data structures like public readonly string|[int](/page/INT) $id;.[48] This combination supports safer data transfer objects in web applications, building on the union type foundation from PHP 8.0.
In Go, there are no native union types, with developers instead emulating them using the empty interface interface{} to hold values of any type, combined with type switches for runtime type inspection and handling.[49] For example, a value can be assigned to an interface{} variable and then inspected via a type switch:
go
var i interface{} = 42
switch v := i.(type) {
case int:
fmt.Println("It's an integer:", v)
default:
fmt.Println("Unknown type")
}
var i interface{} = 42
switch v := i.(type) {
case int:
fmt.Println("It's an integer:", v)
default:
fmt.Println("Unknown type")
}
This approach sacrifices compile-time type safety for flexibility, as the compiler cannot enforce which types might be stored.[49] Since the introduction of generics in Go 1.18, type parameters can use union-like constraints to define sets of allowable types, such as int64 | float64, enabling more type-safe generic functions that approximate variant handling without full union semantics for variables.[50] For instance, a generic sum function might constrain its value type to numeric unions:
go
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
However, these constraints apply only to generic parameters and do not provide true discriminated unions at the value level.[50] Go's design philosophy prioritizes simplicity and explicitness, avoiding complex features like native unions to reduce cognitive load and potential errors in concurrent code. As of November 2025, ongoing proposals such as issue #70752 seek to enable finite type sets as union types via extended constraints, but none have been adopted in the language specification.[18]
Swift, in contrast, supports tagged unions natively through enumerations (enums) with associated values, allowing each case to carry data of different types while ensuring type safety via exhaustive pattern matching.[51] This feature models sum types, where the enum acts as a discriminated union, storing a tag (the case) alongside optional payload data. A canonical example is the Result type, commonly used for error handling:
swift
enum Result<T, E> {
case success(T)
case error(E)
}
let result: Result<String, [Error](/page/Error)> = .success("Hello")
enum Result<T, E> {
case success(T)
case error(E)
}
let result: Result<String, [Error](/page/Error)> = .success("Hello")
Pattern matching occurs via switch statements, which the compiler enforces to cover all cases, preventing runtime surprises:
swift
switch result {
case .success(let [value](/page/Value)):
[print](/page/Print)("Success: \(value)")
case .error(let [error](/page/Error)):
[print](/page/Print)("Error: \(error)")
}
switch result {
case .success(let [value](/page/Value)):
[print](/page/Print)("Success: \(value)")
case .error(let [error](/page/Error)):
[print](/page/Print)("Error: \(error)")
}
This exhaustive checking promotes safe handling of variants, integrating seamlessly with optionals (themselves an enum with associated values) for representing possibly absent data.[51] Swift's enums thus provide a robust alternative to raw unions, emphasizing compile-time guarantees over Go's runtime flexibility.[51]