The OneOf Type in TypeScript

The OneOf Type in TypeScript

Arif sardar

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.

Arif Sardar

Building, learning, and sharing—one line at a time.

Stay updated with my latest content!
Subscribe to my free newsletter by entering your email:

© 2024 Arif Sardar. All Rights Reserved.

Version 4. Powered by Next.JS. Made in India.