This proposal is to restart Proposal: Union Type by frogcjn · Pull Request #404 · swiftlang/swift-evolution · GitHub, which is currently a few that do not have valid follow-up discussions in the forum, such as
[Proposal] Union Type
Multiple Types
Introduction
The generalized Union includes value unoin and type union, typescript supports both:
unoin for value
let value = 1 | 2 | "3"
switch (value) {
case 1: break
case 2: break
case "3": break
case 4.0 // error
}
unoin for type
let value: number | String
if (typeof value === 'number') {
...
}
// This if check can be replaced with `else` directly
if (typeof value === 'string') {
...
}
This proposal only suggests adding unoin for type
, abbreviated as Type Union
, to Swift. Type Union allow a variable to hold a value of multiple types, improving flexibility and type safety.
Motivation
A rewritten draft [Re-Proposal] Type only Unions - #70 by miku1958
In particular, current Swift 6 support Typed throws throws(AError)
, if a developer wants to throw more than one type of error at the same time, there is a lot of extra work to define.
enum ErrorFoo: Error {
// ...
}
enum ErrorBar: Error {
// ...
}
enum Errors: Error {
case foo(ErrorFoo)
case bar(ErrorBar)
}
func doSomething() throws(Errors) {
// ...
}
do {
try doSomething()
} catch .foo(let error) {
// ...
} catch .bar(let error) {
// ...
} catch {
// requirement, otherwise the Error `thrown from here are not handled because the enclosing catch is not exhaustive` shows
}
Proposed Solution
Swift already has a (&
) syntax for merging multiple protocols with an actual type
protocol NSViewBuilder {
// ...
}
protocol NSViewDebuggable {
// ...
}
let view: NSView & NSViewBuilder & NSViewDebuggable
To which adding (|
), allowing a variable to be declared as one of several possible types.
typealias CodablePrimitiveValue = (String | Int64 | UInt64 | Bool | Double)?
let value: CodablePrimitiveValue
switch value {
case let value as String:
print(value)
case let value as Int64:
print(value)
case let value as Bool:
print(value)
case let value as Double:
print(value)
case let value as UInt64:
print(value)
case .none
print("null")
}
enum ErrorFoo: Error {
// ...
}
enum ErrorBar: Error {
// ...
}
func doSomething() throws(ErrorFoo | ErrorBar) {
// ...
}
do {
try doSomething()
} catch let error as ErrorFoo {
// ...
} catch let error as ErrorBar {
// ...
}
Detailed Design
-
Syntax:
- Use the (
|
) symbol to denote a Type Union. - Example:
let value: Int | String
throws(ErrorFoo | ErrorBar)
- Use the (
-
Usage:
-
Assignment to a union-typed variable must match one of the union types.
-
Example:
value = 42 // Allowed value = "hello" // Also allowed value = 3.14 // Error: Type 'Double' does not match 'Int | String'
value = .errorFromFoo // Allowed value = .errorFromBar // Also allowed value = .errorFromURL // Error: Type 'errorFromURL' does not match 'ErrorFoo | ErrorBar'
-
-
Type Inference:
- Swift's type inference system should be able to deduce the type of union-typed variables within the context where they are used.
- Functions and expressions and Typed throws should support union types seamlessly.
-
Pattern Matching:
- Extend Swift's pattern matching to handle Type Union.
- Example:
// ❌ Error: Switch must be exhaustive switch value { case let intValue as Int: print("Integer value: \(intValue)") } // ✅ switch value { case let intValue as Int: print("Integer value: \(intValue)") case let stringValue as String: print("String value: \(stringValue)") }
// ❌ Error: thrown from here are not handled because the enclosing catch is not exhaustive do { try doSomething() } catch let error as ErrorFoo { // ... } // ✅ do { try doSomething() } catch let error as ErrorFoo { // ... } catch let error as ErrorBar { // ... }
-
Interoperability with Existing Types:
- Type Union should integrate smoothly with existing types and collections.
- Example:
let mixedArray: [Int | String] = [1, "two", 3, "four"]
Impact on Existing Code
This feature does not affect existing code. It's purely additive and backward compatible.
Alternatives Considered
- Enums with associated values: While they provide similar functionality, but they are less flexible and require more boilerplate.
- Protocols: Although powerful, they require more complex type constraints and can be less straightforward for simple type union.
- For Typed throws:
throws(ErrorFoo, ErrorBar)
seems like a nice syntax, but it's supposed to be implemented in the same way as a Type Union or Enum(Memory length is stored based on the type with the largest one), so why not extend it with full Type Union support?
Implementation
"existential type" is a more appropriate implementation, where the compiler can automatically integrate the usable method/properties/superclass/protocol of the type being unionized, which has the advantage of implementing something can access the common functions/properties like:
let view: NSTableView | NSCollectionView // both are subclass of NSView
// view can use NSView's content when unwrapped
view.isOpaque = false
This automation is also needed if we want typed throws can use it, so that the compiler can automatically figure out that the common part is a Swift.Error, and use the union type as Swift.Error.
let error: ErrorFoo | ErrorBar // both are Swift.Error
throw error
A rough compiler implementation detail: [Re-Proposal] Type only Unions - #43 by miku1958
Conclusion
Adding Type Union to Swift will simplify handling of variables that can take multiple types and improve code readability, maintainability, and type safety.