Literal Types in short
A type describes the category of a related set of objects. For example, the
Int type describes integers such as 0, 1, 42, -30 and so on. The type of such a value is, well, the type.
val i: Int = 1
With literal types, the type of value is the specific literal used to initialize it. In the following, we specify the type to be the literal
1 and not
val one: 1 = 1
You can use any primitive value as well as any String as a literal type.
val hello: "hello" = "hello"
You can assign values with literal types to their “regular” types.
val num: Int = one val str: String = hello
But you may not do it the other way round:
val two: 2 = 1 // ERROR val second: Int = 2 val three:3 = second // ERROR // Not even... val s: 2 = second // ERROR
At first sight, this feature feels odd and not very useful. After all, the only value you can ever assign to a value with literal type is the value itself. So what’s the point?
The motivating examples listed in the SIP illustrate the usefulness of this feature. However, those examples require a bit of head-scratching to understand and give a feeling that this feature is useful only for advanced type-level programming provided by Shapeless and such. However, that is not so.
A simple example of literal types
Let’s look at an example inspired by TypeScript (which has been supporting literal types for a few years now).
In the Scala.js DOM API, we have the createElement method to create a DOM elements:
def createElement(tagName: String): Element
The typical use of this API is as follows (this example is lifted directly from our code base for LearnRaga).
val script = document.createElement("script").asInstanceOf[HTMLScriptElement] script.src = src script.async = true
Here the knowledge that if we supply “script” as the argument, we get back an
HTMLScriptElement is something not enforced by the code and requires casting before we set the element’s attributes. If we were to use literal types, we can avoid the typecast, simplifying use code as follows.
val script = document.createElement("script") script.src = src script.async = true
How can this be? With literal types, we can create overridden versions of
createElement to return the specific type of the created element.
def createElement(tagName: "script"): HTMLScriptElement = js.native def createElement(tagName: "a"): HTMLLinkElement = js.native ... more such createElement with literal types // Fallback for any specific tag we may not have considered def createElement(tagName: String): Element = js.native
That’s it. We now have a simpler API that expresses the relationship between the tag supplied and the type of the element produced. And we know a simpler motivating example of the literal type feature!