swift-case-paths: key paths for enum cases in Swift
🧰 Case paths extends the key path hierarchy to enum cases.
At a glance
- What is it?
- Case paths fill a gap in Swift's type system. Key paths work for struct and class properties, so you can write generic code that zooms into and modifies a field, but the compiler rejects the same trick on an enum case. This library adds a CaseKeyPath type and a macro that generate them, which is what lets a library hand its users enum ergonomics.
- Who is it for?
- Adopt swift-case-paths if you write a Swift library whose users model their domains as enums and want them to get the same dot-chaining ergonomics they would have with structs, or if you want to write generic code that can read and write the payload of a particular case. Do not adopt it if your enums have no associated values, because there is nothing for a case path to point at.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 10 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- pointfreeco/swift-case-paths1,053 ★ · +1
| Repository | 2026-10-21 | 2026-11-07 |
|---|---|---|
| pointfreeco/swift-case-paths | 1.1k | 1.1k |
1 Stars in the last 17 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The gap in the key path hierarchy
The library's one-line description is that case paths extend the key path hierarchy to enum cases, and the Motivation section explains exactly which gap that closes.
Swift endows every struct and class property with a key path, and the example is the canonical one:
struct User {
let id: Int
var name: String
}
\User.id // KeyPath<User, Int>
\User.name // WritableKeyPath<User, String>That is compiler-generated code that can be used to abstractly zoom in on part of a structure, inspect it and even change it, all while propagating those changes to the structure as a whole. The README notes that key paths are the silent partner of many modern Swift APIs powered by dynamic member lookup, like SwiftUI bindings, and that they also make more direct appearances in the SwiftUI environment property wrapper and in unsafe mutable pointers.
And then there is the hole. Unfortunately, no such structure exists for enum cases:
enum UserAction {
case home(HomeAction)
case settings(SettingsAction)
}
\UserAction.home // 🛑The compiler's complaint is that a key path cannot refer to static member 'home'. And so it is not possible to write generic code that can zoom in and modify the data of a particular case in the enum.
That is a precise statement of the problem, and it is worth sitting with: the language has a first-class abstraction for navigating into a struct, and nothing at all for the other way of modelling data.
The macro and the Cases namespace
The library bridges the gap between structs and enums by introducing case paths, which are key paths for enum cases, and the mechanism for producing them is a macro.
You annotate the enum:
@CasePathable
enum UserAction {
case home(HomeAction)
case settings(SettingsAction)
}And then the cases are reachable through a Cases namespace on the enum, which yields a CaseKeyPath for each case that has an associated value:
\UserAction.Cases.home // CaseKeyPath<UserAction, HomeAction>
\UserAction.Cases.settings // CaseKeyPath<UserAction, SettingsAction>And, like any key path, they can be abbreviated when the enum type can be inferred, so a case path can be written with a leading dot and a type annotation, which is what makes them usable in generic contexts where the enum is a generic parameter.
That is the whole first-party API, and it is small. The interesting part of the library is what it enables for other people, which is the subject of the next section.
The package is MIT licensed, published as CasePaths, with the documentation on the Swift Package Index, the CI badge from the GitHub Actions workflow, and a Slack invite to the pointfree community. The homepage is the pointfree.co collection page for enums and structs.
Why a library would hand this to its users
The README is explicit that by far the most common use of case paths is as a tool inside a library that is distributed to other developers, and it names the libraries: case paths are used in the Composable Architecture, in Swift Navigation and in Parsing, and in many other libraries.
The reasoning is about how people model their domains. If you maintain a library where you expect your users to model their domains with enums, providing case path tools to them can help them break their domains into smaller units.
The Binding example makes the ergonomics concrete. SwiftUI's Binding is a struct with a get and a set, and through dynamic member lookup it supports dot-chaining to derive a new binding to a member of a value, so a binding to a user gives you user.name for a binding to the name. But there are no such affordances for enums, because deriving a binding to just the home case of a destination binding by simple dot-chaining syntax is not possible.
What SwiftUI could do with this library, the README argues, is provide an additional dynamicMember subscript that uses a CaseKeyPath, a key path that singles out a case of an enum:
import CasePaths
extension Binding {
public subscript<Case>(dynamicMember keyPath: CaseKeyPath<Value, Case>) -> Binding<Case>?
where Value: CasePathable {
Binding<Case>(
unwrapping: Binding<Case?>(
get: { self.wrappedValue[case: keyPath] },
set: { newValue, transaction in
guard let newValue else { return }
self.transaction(transaction).wrappedValue[case: keyPath] = newValue
}
)
)
}
}With that defined, a library annotates its enum with the @CasePathable macro and the dot-chaining works, giving destination.home as a Binding<HomeState>? and destination.settings as a Binding<SettingsState>?.
The README calls this an example of how libraries can provide tools for their users to embrace enums without losing out on the ergonomics of structs, and that framing is the library's real contribution.
A derived case binding is optional, and that is the point
One detail in the example deserves its own paragraph, because it explains the shape of the API rather than being a quirk.
The derived binding is optional. destination.home is a Binding<HomeState>? and not a Binding<HomeState>, and the subscript in the extension returns Binding<Case>? as well.
The reason is in the code. The getter reads self.wrappedValue[case: keyPath], which is the payload of that case if the value currently holds that case and nothing otherwise, so the result has to be able to be absent. The setter guards on the new value with a plain guard let, and it writes through the binding's transaction, so a write to the case only happens when the value is actually in that case.
That is the honest signature. An enum binding is a binding to one of several mutually exclusive states, and a binding to a single case can only exist while that case is the current one. Returning an optional rather than trapping or silently doing nothing is what lets the same derived binding be used in a view that is shown for one case and not for another.
It also means the ergonomics are not identical to the struct case. A struct binding to a field always exists, and a case binding is sometimes nil, and any code that uses it has to cope with that. The library does not hide the difference, which is the right call for an abstraction that exists precisely to make enums usable in generic code.
Building it, and the library evolution target
The Makefile is short and tells you how the project is tested, which is worth a look because the target list is unusual for a small Swift package.
There is a test-swift target that runs swift test in parallel, and then runs the whole suite again in release configuration, also in parallel. Running the tests twice in both configurations is how you catch the differences between a debug and an optimised build, which matters for a library whose users will link it into optimised builds.
There is a test-linux target that runs the whole thing inside a container, using the swift:5.6 image, with the repository mounted at the same path, and it installs make inside the container before running the Swift tests. That is a way of testing against a specific toolchain version regardless of what is on the developer's machine, and test-all runs both.
Then there is build-for-library-evolution, which is the most interesting target:
swift build \
-c release \
--target CasePaths \
-Xswiftc -emit-module-interface \
-Xswiftc -enable-library-evolution \
-Xswiftc -DRESILIENT_LIBRARIESThe comment in the file explains the last flag is required to build swift-syntax, and links the pull request. Library evolution is what allows a framework to ship a binary module and still be replaced by a newer version without recompiling the client, and the module interface emission is what produces the .swiftinterface file that makes it work.
The formatting target is swift format --in-place --recursive, so the project uses the formatter that ships with the toolchain rather than a third-party one.
Package manifests, platforms and release state
The repository root carries three package manifests, which is the standard way to support several Swift language modes from one package: Package.swift, [email protected] and [email protected]. The versioned manifests are selected automatically by the toolchain, so a developer on Swift 5.9 gets the 5.9 manifest and a developer on Swift 6 gets the 6.0 one, and the base manifest serves the rest.
There is a Package.resolved alongside them, which pins the dependency versions for reproducible builds, and a .spi.yml for the Swift Package Index, which is the registry that the documentation badge points at. The badges also cover the Swift versions and the platforms, which the index computes from the manifests.
The source is under Sources/ with the tests under Tests/, and the repository has the usual set of supporting files: a Makefile, an .editorconfig, a CODE_OF_CONDUCT.md, a LICENSE file and a .github/ directory with the CI workflow whose badge is at the top of the README.
The release history is regular and fast. Version 1.9.2 came on 2026-08-24, 1.9.3 on 2026-08-28 and 1.10.0 on 2026-08-31, with the last push to the repository on 2026-09-21. Three releases in eight days is the pace of a library that is small, well specified and being used inside a lot of other code.
Editorial conclusion
Adopt swift-case-paths if you write a Swift library whose users model their domains as enums and want them to get the same dot-chaining ergonomics they would have with structs, or if you want to write generic code that can read and write the payload of a particular case. Do not adopt it if your enums have no associated values, because there is nothing for a case path to point at. Before you start, add the @CasePathable macro to the enums you want to expose, and check the library evolution build target in the Makefile if you publish a binary framework, since the library is built with library evolution enabled.
Frequently asked questions
What problem do case paths solve in Swift?
Swift gives every struct and class property a key path, so generic code can zoom in, inspect and modify a field. There is no equivalent for enum cases, because the compiler rejects a key path to a static member. Case paths add that missing layer so generic code can work with the payload of a particular case.
How do I create a case path?
Annotate the enum with the @CasePathable macro and reach its cases through the Cases namespace, so \UserAction.Cases.home gives a CaseKeyPath<UserAction, HomeAction>. As with any key path, it can be abbreviated with a leading dot when the enum type can be inferred.
Why is the binding derived from a case path optional?
Because the value may currently be in a different case, so the getter that reads the case payload can only produce a value sometimes. The example in the README derives destination.home as a Binding<HomeState>?, and the setter guards on the new value before writing it back through the binding's transaction.
Which libraries already use case paths?
The README names the Composable Architecture, Swift Navigation and Parsing, and says case paths are used in many other libraries. The intended use is inside a library, so that users who model their domains as enums can break them into smaller units without losing the dot-chaining ergonomics of structs.
How is swift-case-paths tested and released?
The Makefile runs swift test in parallel in both debug and release configurations, has a container target that runs the suite on the swift:5.6 image, and has a build-for-library-evolution target that emits a module interface with library evolution enabled. The package ships three manifests, for Swift 5.9, Swift 6.0 and the default, and the recent releases are 1.9.2, 1.9.3 and 1.10.0 through 2026-08-31.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pointfreeco-swift-case-paths)Star history chart for your README
[](https://hysenlabs.com/projects/pointfreeco-swift-case-paths)Was this useful?