Goal: Two semantically different types, with the exact same shape, should have a different Swift type. Creating a new type should be as light-weight as possible.
Introduction
It is often useful to create a new distinct type, based on a previous type. We propose newtype
as a language feature to make this possible in an easy and concise way:
newtype RecordId = Int
This should create a new type, that isn't equal to the original type on which it is based. In particular, this shouldn't compile:
func pow(_ n: Int) {}
func get(id: RecordId) {}
let someId: RecordId = RecordId(value: 42)
pow(someId) // Error, RecordId is not an Int
let someInt: Int = 42
get(id: someInt) // Error, Int is not a RecordId
Note that in this way, newtype
is different from the existing typealias
, because it creates a new type, that is not freely exchangeable for the original type.
Semantically, this newtype
declaration would expand to what can be done currently with a new struct:
struct RecordId {
var value: Int
}
Motivation
Using a new distinct type for domain models helps to prevent mistakes and improves clarity of the codebase. For example, when two identifiers have the same type, it is easy to accidentally use the wrong variable when calling a function:
struct Person {
let id: String
let name: String
}
struct Building {
let id: String
let owner: Person
let title: String
}
func scrollToPerson(withId: String) {}
scrollToPerson(withId: mainBuilding.id) // Uncaught accidental error
When we manually create a distinct type, it becomes possible for the compiler to prevent this mistake:
struct Person {
struct Identifier { // Without newtype
var value: String
}
let id: Identifier
let name: String
}
struct Building {
struct Identifier { // Without newtype
var value: String
}
let id: Identifier
let owner: Person
let title: String
}
func scrollToPerson(withId: Person.Identifier) {}
scrollToPerson(withId: mainBuilding.id) // Error, incompatible type
scrollToPerson(withId: mainBuilding.owner.id) // Correct
Manually creating new types in cases like these would improve safety and clarity at the point of use. But manually creating a new type using a nested struct creates a lot of visual overhead, preventing it from becoming a common pattern. This is especially true for types that can just as easily be represented using an existing type, like the identifiers in the example above.
Lowering the cost of declaring new types from existing types will encourage users to write more strongly typed code, which improves code quality and readability.
Proposed solution
To make it easier to create a new distinct type based on an existing type, we propose the newtype
feature. With this feature the example becomes:
struct Person {
newtype Identifier = String
let id: Identifier
let name: String
}
struct Building {
newtype Identifier = String
let id: Identifier
let owner: Person
let title: String
}
This newly created type generates a struct box around the existing type in a value
field. It can be used in the following way:
let personId = Person.Identifier(value: "Jane")
let stringValue = personId.value
Just like any other type in Swift, it is possible to write an extension on Person.Identifier to implement protocols and add members.
No automatic protocol implementations
Note that the newly created type doesn’t implement any of the protocols implemented by the original type. Nor does it implement any of the members on the original type. This fully separates the new and original types and prevents semantically incorrect behaviour. In the Person identifier example, this means it doesn’t implement CustomStringConvertable
and two Person.Identifiers can’t be concatenated using +
.
There are valid use cases for automatic forwarding of (some) protocols or members from the original type to the new type, but this is (explicitly) not part of this proposal. Several different approaches to implement this functionality are discussed in the paragraph Possible Future Extensions.
We believe the newtype
feature to be a useful addition to the Swift language, even without automatic forwarding.
Use cases
We see 3 primary use cases that we’ve extracted from existing codebases and public discussions.
1. Identifiers
As described above:
struct Person {
newtype Identifier = String
let id: Identifier
let name: String
}
Identifiers are used in a lot of applications, these often are numbers or strings and important not to mix up. Example of a online discussion about creating a distinct type for identifiers: https://twitter.com/cocoaphony/status/756538935707365376
2. Separating semantically distinct fields
In an app dealing with shipments of packages, it is important to never mixup the sender and receiver fields, even though they’re both of the same Address type:
struct Shipment {
newtype Identifier = String
newtype Sender = Address
newtype Receiver = Address
var id: Identifier
var sender: Sender
var receiver: Receiver
var contents: String
}
This example is taken from our app for the Dutch Postal Services. See this gist for how this is implemented without the newtype feature. The version with manually written types is considerably harder to understand.
3. Phantom types
Phantom types can also easily be created using newtype
, taking the example from objc.io:
newtype FileHandle<T> = Foundation.FileHandle
Source: Functional Snippet #13: Phantom Types · objc.io
Possible Future Extensions
Previous discussions about newtype have gotten stuck on the topic of protocol implementations/forwarding. This is why we have kept it out of scope for now. However adding protocol forwarding would make this a more useful feature.
As a motivating example, say we have a Point with separate X and Y types (from LoĂŻc Lecrenier):
struct Point {
newtype X = CGFloat
newtype Y = CGFloat
var x: X = 0.0
var y: Y = 0.0
}
With this type, we prevent accidentally mixing up the Point.X and Point.Y types, but we also can’t calculate with them. We’d need to add an extension with implementations for all numeric operators. Here, some form of automatic protocol implementation would be useful.
Approaches:
1. Automatic implementation of Hashable & Equatable
Just hardcode these two protocols in the compiler, for all newtypes. For the same reason that these are already hardcoded for value types.
2. Add a Newtypable protocol
Have all newtypes automatically implement a new protocol Newtypable. That way users only have to write a manual implementation once using conditional conformance. See example gist.
3. Add a Haskell-like deriving
syntax
Similar to Haskell, we could add a new language feature where the user can explicitly opt-in to certain protocols by specifing them:
newtype X = CGFloat deriving (FloatingPoint)
In the future, this could also be extended to included the latest state-of-the-art in Haskell, deriving-via: https://www.kosmikus.org/DerivingVia/deriving-via-paper.pdf
Previous discussions
- [Discussion] What is the future of tuples in Swift?
- Proposal: newtype feature for creating brand new types from existing types
- https://twitter.com/cocoaphony/status/756538711916081152
- Interesting discussions on Swift Evolution — Erica Sadun
- Support for newtype feature/typesafe calculations
- Opaque result types - #177 by Douglas_Gregor
--
Tom Lokhorst, Q42 (https://twitter.com/tomlokhorst)
Mathijs Kadijk, Q42 (mac-cain13 (Mathijs Kadijk) · GitHub)