# \[Pitch\] Property-Based Type Narrowing and Union Types

**URL:** <https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871>\
**Category:** Pitches\
**Tags:** swift\
**Created:** [March 26, 2025, 10:33pm UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871 "2025-03-26T22:33:45Z")\
**Posts on this page:** 16\
**Page:** 1

<div class="post-metadata">

**Author:** ![jpmcglone](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jpmcglone/32/40235_2.png) [@jpmcglone](https://forums.swift.org/u/jpmcglone)\
**Post date:** [March 26, 2025, 10:33pm UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/1 "2025-03-26T22:33:45Z")

</div>

### Title:

**Property-Based Type Narrowing and Union Types**

### Author:

John McGlone

### Introduction:

This proposal introduces two interrelated enhancements to the Swift type system:

1. **Value-based union types** (`A | B`)
2. **Flow-sensitive type narrowing based on property checks**

These features enable safer and more expressive control flow when working with values of multiple possible types, similar to what languages like TypeScript and Kotlin support.

* * *

### Motivation:

Currently, Swift relies heavily on manual type casting (`as?`, `as!`) and enums with associated values to handle values that may conform to multiple types. This creates boilerplate and weakens readability in many situations, especially when working with protocols or loosely structured models.

In contrast, TypeScript allows developers to write more expressive code by narrowing union types based on:

- Property presence (`'meow' in animal`)
- Literal discriminants (`type == "cat"`)
- Type guards (`instanceof`, `typeof`)

We propose a similar system for Swift, allowing smarter narrowing in `if`, `switch`, and similar control flow blocks.

* * *

### Proposed Solution

#### 1. Introduce Union Types

Enable declaring a parameter that can be one of multiple value types:

```swift
func handle(_ input: Cat | Dog) { ... }

```

#### 2. Enable Flow-Sensitive Type Narrowing by Property Presence

Allow Swift to infer the type based on property existence, so this code becomes valid:

```swift
func speak(_ animal: Cat | Dog) {
    if animal.meow != nil {
        // animal is inferred as Cat
        animal.meow()
    } else {
        // animal is inferred as Dog
        animal.bark()
    }
}

```

#### 3. Narrowing with Literal Discriminants

When using a shared `type` field or enum-like value:

```swift
struct Cat {
    let type = "cat"
    let meow: () -> Void
}
struct Dog {
    let type = "dog"
    let bark: () -> Void
}

func speak(_ animal: Cat | Dog) {
    if animal.type == "cat" {
        animal.meow() // inferred as Cat
    } else {
        animal.bark() // inferred as Dog
    }
}

```

* * *

### Detailed Design

This would require:

- Introducing **structural union types**
- Enhancing the compiler's **type inference engine** to track property accesses across branches
- Possibly introducing **syntactic sugar** like `'property' in object` or `has(object.property)`

* * *

### Source Compatibility

This would be an **additive change** to the language and would not break existing Swift code.

* * *

### Alternatives Considered

- Continue relying on `as?`, enums, and protocols
- Use enums with associated values (heavier but already supported)
- Type erasure patterns (boilerplate-heavy)

* * *

### Conclusion

By adopting property-based type narrowing and union types, Swift becomes more expressive, reducing boilerplate and improving safety when dealing with heterogeneous types — especially in modern app development and data-driven UIs.

---

<div class="post-metadata">

**Author:** ![Lancelotbronner](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/lancelotbronner/32/22071_2.png) [@Lancelotbronner](https://forums.swift.org/u/Lancelotbronner)\
**Post date:** [March 27, 2025, 1:11am UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/2 "2025-03-27T01:11:01Z")

</div>

While I’d like to eventually see union types I’m not comfortable with relying that much on type inference, it’s already quite slow.

The second point relies on inference, something like `is Type` or `as? Type` to make it explicit instead would be the best of both worlds I think.

For your third point, you’re reimplementing existing language features, like protocols/classes so I don’t think this is necessary.

---

<div class="post-metadata">

**Author:** ![jpmcglone](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jpmcglone/32/40235_2.png) [@jpmcglone](https://forums.swift.org/u/jpmcglone)\
**Post date:** [March 27, 2025, 1:29am UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/3 "2025-03-27T01:29:35Z")

</div>

I really like your suggestion of using explicit type checks like this:

```swift
if animal is Cat {
    animal.meow()
} else if animal is Dog {
    animal.bark()
}

```

The feature I'm describing though aligns more closely with TypeScript's discriminated unions. Rather than using type checks, it relies on property-based narrowing. A clearer and more practical example than the silly animal illustration would look like this:

```swift
struct ResponseData {
    let status = "success"
    let data: Data
}

struct ResponseError {
    let status = "error"
    let error: Error
}

// Hypothetical Union Type Declaration
typealias NetworkResponse = ResponseData | ResponseError

func handleResponse(_ response: NetworkResponse) {
    // Property-based narrowing using the literal discriminant `status`
    if response.status == "success" {
        print("Received data of length: \(response.data.count)")
    } else {
        // response.error is *not* nil because response.status is *not* "success"
        print("An error occurred: \(response.error.localizedDescription)")
    }
}

```

---

<div class="post-metadata">

**Author:** ![amyworrall](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/amyworrall/32/23701_2.png) [@amyworrall](https://forums.swift.org/u/amyworrall)\
**Post date:** [March 27, 2025, 3:31am UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/4 "2025-03-27T03:31:24Z")

</div>

Would this now be valid?

```swift
if foo != nil {
  foo.doSomething()
}

```

(I know that `if let foo` is as easy to type -- but if we're doing inference like that, it seems that we should apply it everywhere.)

---

<div class="post-metadata">

**Author:** ![YOCKOW](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/yockow/32/20636_2.png) [@YOCKOW](https://forums.swift.org/u/YOCKOW)\
**Post date:** [March 27, 2025, 4:07am UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/5 "2025-03-27T04:07:03Z")

</div>

🤔I think "Union Types" is listed in [Commonly Rejected Changes](https://github.com/swiftlang/swift-evolution/blob/main/commonly_proposed.md).

> - [Disjunctions (logical ORs) in type constraints](https://forums.swift.org/t/returned-for-revision-se-0095-replace-protocol-p1-p2-syntax-with-any-p1-p2/2855): These include anonymous union-like types (e.g. `(Int | String)` for a type that can be inhabited by either an integer or a string). "[This type of constraint is] something that the type system cannot and should not support."

* * *

> [@amyworrall](#):
>
> Would this now be valid?
> 
> ```swift
> if foo != nil {
> foo.doSomething()
> }
> 
> ```

That is source breaking change:

> [@Let's fix \`if let\` syntax](https://forums.swift.org/t/lets-fix-if-let-syntax/48188/30):
>
> About `if value != nil`, it is really problematic. Currently this code works.
> 
> ```swift
> var value: Int? = 42
> // some process
> if value != nil {
> // some process     
> // finally make value nil
> value = nil
> }
> 
> ```
> 
> If Swift introduces `if value != nil` that implicitly unwraps `value`, it starts causing error, because `value` inside `if` is no longer `Int?` but `Int`. It's **source breaking** change actually.

---

<div class="post-metadata">

**Author:** ![bbrk24](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/bbrk24/32/27370_2.png) [@bbrk24](https://forums.swift.org/u/bbrk24)\
**Post date:** [March 27, 2025, 5:59pm UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/6 "2025-03-27T17:59:04Z")

</div>

> [@Let's fix \`if let\` syntax](https://forums.swift.org/t/lets-fix-if-let-syntax/48188/30):
>
> ```swift
> var value: Int? = 42
> // some process
> if value != nil {
> // some process     
> // finally make value nil
> value = nil
> }
> 
> ```

Kotlin works around this problem by only allowing this when `value` is an immutable local or stored variable, and not when it’s mutable or has a getter. In Swift terms, only `let`s can do narrowing here, not `var`s (and obviously not return values from functions).

---

<div class="post-metadata">

**Author:** ![flexlixrup](https://avatars.discourse-cdn.com/v4/letter/f/50afbb/32.png) [@flexlixrup](https://forums.swift.org/u/flexlixrup)\
**Post date:** [March 27, 2025, 9:33pm UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/7 "2025-03-27T21:33:32Z")

</div>

As with the previous pitch on union types, I‘m still against a change to allow them.  
The Swift type system is really strict, and this is a core principle of Swift, and allowing union types would soften this

Also, `if animal.meow != nil {` is wrong from a logical point of view. `nil` does not indicate a non-existent object/value in Swift, but indicates that a specific optional type has no value assigned to it.  
Your syntax would mean that both `Cat` and `Dog` have a `(meow() → Void)?` and a `(bark() → Void)?` variable which is not set in one of the 2 types.

---

<div class="post-metadata">

**Author:** ![sveinhal](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/sveinhal/32/3992_2.png) [@sveinhal](https://forums.swift.org/u/sveinhal)\
**Post date:** [March 31, 2025, 1:10pm UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/8 "2025-03-31T13:10:37Z")

</div>

As with previous pitches of this kind, I support a way to spell out non-nominal sum types, aka. unnamed enums.

Just as a tuple is a non-nominal struct (product type), we should support non-nominal enums.

| | Sum type | Product type |
| --- | --- | --- |
| _nominal_ | `enum Animal` | `struct AgeAndName` |
| _non-nominal_ | `(Cat | Dog)` | `(Int, String)` |

This would fill a gap in the existing type system, without creating a true union type or introduce fundamentally new concepts. We could reuse almost all existing primitives and syntax from enums and tuples:

- Use `case let` to pattern match, like `enum`
- Use `switch`-statements like `enum`
- Use `case .0(let cat)` for unnamed cases, similar to `.0` for unnamed tuple elements
- Allow labels `(dog: Dog | cat: Cat)` similar to how tuples can be labelled `(name: String, age: Int)`

The trivial empty sum `()` (aka a never-type) would be disallowed, to avoid ambiguation with the empty product `()` (aka. the unit type `Void`)

---

<div class="post-metadata">

**Author:** ![jpmcglone](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jpmcglone/32/40235_2.png) [@jpmcglone](https://forums.swift.org/u/jpmcglone)\
**Post date:** [March 31, 2025, 1:19pm UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/9 "2025-03-31T13:19:13Z")

</div>

The Kotlin approach is great. This would be sufficient, in my opinion.

---

<div class="post-metadata">

**Author:** ![allevato](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/allevato/32/385_2.png) [@allevato](https://forums.swift.org/u/allevato)\
**Post date:** [March 31, 2025, 1:47pm UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/11 "2025-03-31T13:47:28Z")

</div>

> [@sveinhal](#):
>
> As with previous pitches of this kind, I support a way to spell out non-nominal sum types, aka. unnamed enums.

I'd be wary of introducing a new kind of non-nominal type because it would naturally require a bunch of special case logic as tuples already have, and we still hit limitations of tuples because of that.

So instead of using that as a starting point, I think I'd rather have the tools to build up a named variadic generic `enum` type. Completely strawman syntax below and probably broken in many ways, but if we assume we could express labeled generic arguments where the labels themselves could be arbitrary (i.e., **not** fixed labels that separate sequences of unlabeled arguments, hence the location of `label` relative to `each` here), then I could imagine something like this:

```swift
enum Animal<each label: T> {
  repeat case each label(each T)
}

// then this
Animal<dog: Dog, cat: Cat>

// would expand to something like
enum Animal<dog: Dog, cat: Cat> {
  case dog(Dog)
  case cat(Cat)
}

```

And maybe if `label` is omitted, then the index is used instead.

That would be strictly more powerful/flexible, because you could add additional fields to the payloads if you wanted:

```swift
enum Located<each kind: ASTNode> {
  repeat case each kind(each ASTNode, location: SourceLocation)
}

let x: Located<decl: Decl, expr: Expr, stmt: Stmt>
switch x {
case .decl(let decl, let location): // ...
case .expr(let expr, let location): // ...
case .stmt(let stmt, let location): // ...
}

```

Then, that foundation could be used to build up what you're talking about, if deemed useful: the standard library could propose to add an `enum Either<each choice: Choice>` type, and the compiler could add `(a: A | b: B | ...)` as _syntax sugar_ for `Either<a: A, b: B, ...>` instead of making it an entirely new kind of non-nominal.

(I'm already seeing that `each kind: T` would be interpreted as a pack of parameters named `kind` that conform to `T`, so that syntax is already not viable. But you get the idea.)

---

<div class="post-metadata">

**Author:** ![sveinhal](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/sveinhal/32/3992_2.png) [@sveinhal](https://forums.swift.org/u/sveinhal)\
**Post date:** [April 1, 2025, 9:08am UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/12 "2025-04-01T09:08:26Z")

</div>

Sure variadic generic `enum`s is another hole to be filled, and I welcome it.

However, it's pretty heavy for a lot of one-offs, where I just need a function to take either a `Hashable` or `Void`. Or I need either a `Decimal` or a `FloatingPoint`. You know, all the same cases where a tuple is much more light weight to spell out than making a `struct`.

But I'm not too versed in the intricacies of implementation here, speaking strictly from a point of view of usability at call sites and simplifying the mental model by filling non-spellable gaps. I might be asking too much.

---

<div class="post-metadata">

**Author:** ![glukianets](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/glukianets/32/34964_2.png) [@glukianets](https://forums.swift.org/u/glukianets)\
**Post date:** [April 1, 2025, 10:41am UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/13 "2025-04-01T10:41:01Z")

</div>

I was really in need of this on multiple occasions, even considered to pitch this myself at some point.

The latest case was with my parser combinator library, where I really wanted `AnyOf { A(); B() }` to return `(A.Result | B.Result)`.

Another good use case could be with the new typed throws:

```swift
turnaround<E1, E2>(
    frobnicate: () throws(E1) -> Void,
    defrobnicate: () throws(E2) -> Void
) throws(E1 | E2)

//Or

foo<E>(bar: () throws(E) -> Void) throws(E | InternalFooError) { ...

```

The matching syntax should resemble those for regular enums, except sometimes labels are implicit/numerical:

```swift
switch (10 |) as (Int, Float) {
    case .0(let intVal): break
    case .1(let floatVal): break 
}

// or

switch (float: 20.0) as (int: Int, float: Float) {
    case .int(let intVal): break
    case .float(let floatVal): break 
}

```

---

<div class="post-metadata">

**Author:** ![xwu](https://avatars.discourse-cdn.com/v4/letter/x/b487fb/32.png) [@xwu](https://forums.swift.org/u/xwu)\
**Post date:** [April 1, 2025, 2:30pm UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/14 "2025-04-01T14:30:22Z")

</div>

> [@sveinhal](#):
>
> However, it's pretty heavy for a lot of one-offs, where I just need a function to take either a `Hashable` or `Void`. Or I need either a `Decimal` or a `FloatingPoint`.

These are examples of already-identified limitations of the language that would be much sooner solved than union types:

`Void` _should_ conform to `Hashable` (it's already an approved but expired Evolution proposal), and we _should_ have a modern decimal floating-point type that conforms to `FloatingPoint` (it's an open issue in Swift Numerics; and indeed, that's why we have `BinaryFloatingPoint` as a refinement to begin with).

---

<div class="post-metadata">

**Author:** ![technogen](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/technogen/32/45489_2.png) [@technogen](https://forums.swift.org/u/technogen)\
**Post date:** [April 2, 2025, 4:51pm UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/15 "2025-04-02T16:51:48Z")

</div>

> [@xwu](#):
>
> These are examples of already-identified limitations of the language that would be much sooner solved than union types:

What about use cases outlined by @glukianets above?

---

<div class="post-metadata">

**Author:** ![jameesbrown](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jameesbrown/32/40044_2.png) [@jameesbrown](https://forums.swift.org/u/jameesbrown)\
**Post date:** [April 8, 2025, 4:30am UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/16 "2025-04-08T04:30:07Z")

</div>

Similar to earlier replies, I could see a syntax sugar that embraces enums with associated values:

```swift
/// `#enum(String, Int)` generates an enum.
// invisible to the rest of the code somehow?
enum _E {
  case string(String)
  case int(Int)
}

func isEven(x: #enum(String, Int)) -> Bool {
  switch x {
  case let .string(string):
    string.count.isMultiple(of: 2)
  case let .int(int):
    int.isMultiple(of: 2)
  }
}
isEven(x: "Hello")
isEven(x: 2)

```

---

<div class="post-metadata">

**Author:** ![hishnash](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/hishnash/32/9519_2.png) [@hishnash](https://forums.swift.org/u/hishnash)\
**Post date:** [April 8, 2025, 7:08am UTC](https://forums.swift.org/t/pitch-property-based-type-narrowing-and-union-types/78871/17 "2025-04-08T07:08:23Z")

</div>

While I do want to see union types, in particular for handling errors.

I do not think prompters on these should be inferred to be nill if the value is not of that type.

Instead I think when operating on the union of the type you can only access properties/functions that have identical call signatures. Then you can use `is or as` type checks for branching type specific code.

You could imagine impmenteing this today within swift by having a pre-compiler step ad hock creates protools for any union it spots and populate the protol with all the methods/properties the types have in common with each other.

eg

```swift
func speak(_ animal: Cat | Dog) {
    if animal is Cat.self {
        // animal is inferred as Cat
        animal.meow()
    } else {
        // animal is inferred as Dog
        animal.bark()
    }
}

```

Could be converted pre compile into

```swift
private protocol _CatOrDog {
    var type: String { get }
}

private extension Cat: _CatOrDog {}
private extension Dog: _CatOrDog {}

func speak(_ animal: _CatOrDog) {
    if animal is Cat.self {
        // animal is inferred as Cat
        animal.meow()
    } else {
        // animal is inferred as Dog
        animal.bark()
    }
}

```
