The OneOf Type in TypeScript
Published on 16th Sep, 2024
9 min read
Union types in TypeScript are a powerful feature that allows you to create flexible type definitions. However, they come with a flaw: they don't enforce exclusivity between type members. For instance, a union of several types might allow an object to have properties from multiple types simultaneously—something that isn’t always desirable. In this blog, we'll discuss how to implement the OneOf
type to enforce exclusivity and correct this behavior, improving type safety in your applications.
The Problem with Union Types
Union types in TypeScript allow you to define a type that can be one of several other types. While this is useful, it doesn't enforce mutual exclusivity between the fields of those types. Here's a common example:
You’d expect a Car
object to either have a batteryCapacity
, a fuelCapacity
, or a solarPanelArea
property, but not more than one at a time. However, TypeScript won’t complain if you create a Car
that has all three fields:
This clearly shows that TypeScript unions don’t enforce the exclusivity you might want. To solve this, we need to create a custom type that represents a union of all properties of the given types.
Understanding the Need for OneOf
When defining a union of types in TypeScript, it's common to expect exclusivity between different fields, but that doesn’t happen by default. This is where the OneOf
type comes in.
The OneOf
type ensures that an object can only have one of several specified properties at a time, making it an essential tool for scenarios where you want to prevent the overlap of properties that logically shouldn't exist together.
Let’s look at the car example again:
Without OneOf
, the Car type allows you to create a car with all three properties — batteryCapacity
, fuelCapacity
, and solarPanelArea
— even though it’s not logically possible for a car to be electric, gas-powered, and solar-powered at the same time.
This is where OneOf
comes in, solving the flaw by enforcing that only one of these types is valid for a car. With OneOf
, if you try to define a car with both batteryCapacity
and fuelCapacity
, TypeScript will throw an error, as expected.
The Concept of Exclusive Type Enforcement
When working with union types, we often want to ensure that only one of the possible types is chosen, while the others are automatically excluded. This is particularly relevant when defining complex objects where only certain combinations of properties make sense.
The key concept here is exclusivity: we want to allow only one of the possible types (or sets of properties) at a time, and ensure that no overlap occurs. One way to achieve this is to force all non-relevant properties of other types in the union to become never
. In TypeScript, never
is a type that represents values that never occur. By using never
, we can signal to TypeScript that certain properties should never exist together, effectively preventing them from being used at the same time.
In simpler terms, if you choose one type, all other fields that belong to other types will automatically become never
, making it impossible to accidentally include them.
Now the Car
type enforces exclusivity between the ElectricCar
, GasCar
, and SolarCar
types, ensuring that only one of the three can be used at a time. If you try to create a car with multiple properties, TypeScript will throw an error, preventing you from making a logically incorrect object.
But, manually setting the never
type for each property can be cumbersome and error-prone, especially when dealing with a large number of types or properties. To simplify this process, we can create a custom type that automatically enforces exclusivity between the properties of different types.
Implementing the OneOf
Type
To create the OneOf
type, we need to define a custom type that merges all properties of the given types and ensures that only one of them is valid at a time. This type will automatically set non-relevant properties to never
, enforcing exclusivity between the types.
So, first we need to create a utility type that merges all properties of the given types:
The MergeTypes
type takes an array of types and merges their properties into a single type. It uses a recursive conditional type to iterate over the array and merge the properties of each type into the result type.
Next, we need to define a utility type that extracts only the properties of the first type that are not present in the second type:
The OnlyFirst
type takes two types F
and S
and returns a new type that contains only the properties of F
that are not present in S
. It uses mapped types to iterate over the keys of S
and exclude any keys that are already present in F
.
Finally, we can define the OneOf
type that enforces exclusivity between the properties of the given types:
The OneOf
type takes an array of types and recursively compares each type with the merged properties of all types. It uses the OnlyFirst
utility type to extract the exclusive properties of each type and combines them into the result type. The process continues until all types have been processed, resulting in a type that enforces exclusivity between the properties.
Now, we can use the OneOf
type to define the Car
type with exclusivity between the ElectricCar
, GasCar
, and SolarCar
types:
With the OneOf
type, the Car
type enforces exclusivity between the ElectricCar
, GasCar
, and SolarCar
types, ensuring that only one of the three can be used at a time. If you try to create a car with multiple properties, TypeScript will throw an error, preventing you from making a logically incorrect object.
Final Code
Here's the final code that defines the OneOf
type and uses it to enforce exclusivity between the ElectricCar
, GasCar
, and SolarCar
types:
With this implementation, you can now create a Car
object that enforces exclusivity between the ElectricCar
, GasCar
, and SolarCar
types, improving type safety in your applications.
Conclusion
The OneOf
type in TypeScript addresses a critical flaw in union types by enforcing exclusivity between properties that shouldn't coexist. By utilizing never
for conflicting fields, it prevents invalid combinations of data and ensures that only one of the defined types is selected at a time. This approach is especially valuable in scenarios where logical consistency is paramount, such as distinguishing between different types of cars, messages, or any other mutually exclusive structures.
Implementing this pattern in your code not only improves type safety but also enhances the clarity and maintainability of your TypeScript applications. As TypeScript continues to evolve, tools like OneOf
provide a powerful means to create robust and error-free systems that adhere strictly to your intended design logic.