Reading Time: 12 minutes

Literal types will let you define a type as an enumeration of possible values. This is useful in the use cases when a variable or function call can only take one out of a small set of possible values, for example, like days in a week (Saturday, Sunday, etc.) or HTTP request methods (GET, POST, DELETE, etc.). 

Using literal types allows you to be more specific in what the type checking should validate as constraints. This would end in a code that is less error-prone, because you can limit the domain for function calls, assignment in variables, and return types of functions using literal types. Also, you can use literal types combined with function overloading to isolate behaviors to specific inputs in different function definitions.

latest report
Learn why we are the Leaders in API management and iPaaS

In this post, we’ll go over:

  • What is a literal type?
  • How to declare types using literal types.
  • How to use them as constraints.
  • How it can help you:
    • Type checking errors with literal types
    • Define different behaviors for functions in runtime, based on literal types.

To understand this blog post fully, first review the basics of the type system of DataWeave or check out the documentation

This feature is supported in DataWeave 2.3.0. supported by Mule 4.3 and later.

What is a literal type?

We can think about a type as a set of values, like the type Number represents all the possible numbers, for Strings you have as values all the possible sequences of characters. A literal type is a set of just one value, it represents only one literal value:

Using literal types with union types (used to compose types) you could define a type that represents an enumeration of literal values. For example, we can define the type Weather.

The following literal types are included to the DataWeave type system:

  • String literal types
  • Number literal types
  • Boolean literal types

Literal types in action

Declaration of a type using literal types:

%dw 2.0
output json
//Definition of the type Weather using literal types
type Weather = "cloudy" | "sunny" | "rainy" | "stormy"
 
//Use of the type weather as a constraint for function calls
fun weatherMessage(todayWeather: Weather) = 
      "The weather for today is: " ++ todayWeather
---
weatherMessage("sunny")

Here is how we defined the type Weather mentioned in the section “what is a literal type?” — using literal types with union types. 

You can use literal types to limit the domain for a function call, so you don’t need to worry about getting called by something outside of the domain.

As you can see, the type Weather is being used as a type constraint for the first parameter in the function weatherMessage, so if anything that is not a valid value for the type Weather is used to call the function, the type checking will raise an error, marking it in the IDE.

Without using literal types we should check manually if the string given as an argument is one of the expected ones, or fail if it is not. 

%dw 2.0
import fail from dw::Runtime
output json
//Same function without using literal types.
fun weatherMessage(todayWeather: String) =
   if (["sunny", "rainy", "cloudy", "stormy"] contains todayWeather)
       "The weather for today is: " ++ todayWeather
   else
       fail("Weather not supported")
---
weatherMessage("sunny")

In this case, we have just one function that receives a Weather, but if we had multiple functions, we should create a parseWeather function to check if the String is a Weather and add it at the beginning of every function, making the code more verbose, and less clear.

You can use literal types inside Composite types (Array, Object, Function) to define a new type. For example, it can be used inside an Object type.

%dw 2.0
output json
 
type Weather = "cloudy" | "sunny" | "rainy" | "stormy"
 
//Use of the type Weather in an Object Type
type detailedWeather = {temperature: Number, weather: Weather}
 
var worstPossibleWeather: detailedWeather = {temperature: -2, weather: "stormy"}
---
worstPossibleWeather

This example defines a type detailedWeather as an Object type with a set of constraints for their key-value pairs. It has the constraint of having the keys temperature and weather defined and the value of those keys should be a Number and meet the constraint of the defined type Weather respectively.

Function overloading with literal types:

Imagine this simple use case, where we made a request to a weather API that responds with the actual climate conditions of a specific place. Within all the information there is the actual weather that could be one of these values: “cloudy”, “sunny”, “rainy”, “stormy.” We want to send a message in a chat with a recommendation based on the actual weather. 

We can solve the part of choosing the recommendation easily using function overloading with literal types:

%dw 2.0
output application/json
 
type BadWeather = "stormy" | "rainy"
 
fun recommendation(actualWeather: "sunny") = "You should take your sunglasses!"
fun recommendation(actualWeather: "cloudy") = "Great day! It is cloudy out there"
fun recommendation(actualWeather: BadWeather) = "You should take your umbrella!"
---
recommendation(payload.weatherCondition)

In this case if payload.weatherCondition has as a value the String “sunny,” the function definition that will be executed is going to be the first one and will output the message: “You should take your sunglasses!” If it were of type “stormy” or “rainy” (BadWeather) the third function definition would be executed. This is because the function dispatching algorithm decides at runtime what function to execute based on the type of the argument, and because we used literal types, we can define the different behaviors for different literal values. 

This could be done with an if statement or a pattern matching as well, but If you have a more complex behavior for each literal type, you could consider this approach as a better option because:

  • You can isolate a problem better because of the separation of the behaviors in different functions.
  • The code is cleaner and easier to read, instead of having a huge function with if statements or pattern matching expressions.

Also If you have multiple functions that works on those literal values, this approach could be a better option because:

  • If the API adds another literal value like “windy”, you just need to add that value in the type declaration of Weather and you will have all the functions that use that type working (only one place to change), instead with the if statement approach you should change all the functions that use those if statements to support the new value (possibly multiple changes).

Conclusion

Literal types are useful when you need to work with an enumeration of possible values. This allows you to have a code less error prone, thanks to using it with the type checking; change the behavior of functions based on literal values thanks to using it with function overloading, and have a cleaner code that is easier to read and modify.

If interested in learning more about DataWeave, check out our newest Developer Tutorials.


Series Navigation<< DataWeave: Taking advantage of the type system