Monday, February 01, 2021

Literal and Template Literal Types

This blog post will look at Literal and Template Literal Types. It is part of the Introduction to Advanced Types in TypeScript series.

In the post, Union and Intersection Types in TypeScript, the idea of viewing types with the perspective of Set theory was introduced. The key idea is to see types more or less as sets, and values of a type as elements that belong to a set. If this idea of viewing types from the perspective of Set theory is new to you, then I’ll advise to take a pause and read Union and Intersection Types in TypeScript before continuing with the rest of the post, because it is a useful perspective to understand literal types.

What are literal types?

Literal types are types created from literal values

Seen another way, a literal type is a mechanism which TypeScript provides, that allows us to create types from values from selected primitive types in JavaScript.

Primitive types are values that are not objects and hence have no methods. JavaScript has 7 of these primitive types namely:  string, number, bigint, boolean, undefined, symbol, and null

TypeScript then allows the creation of types from 3 of these primitive types. These selected primitive types are string, numbers, and booleans.

So basically literal types are types that are created from literal values of string, numbers, or booleans.

What exactly does creating a type from a literal value mean? 

If we delineate a type or seen another way, a set based on a particular value, that means we are saying that a particular value represents the type (or seen another way, represents the set) that is being defined. 

This means a literal value like "Hi" stops being just a value but a type.

The question then is, what type of element can then be accepted in such a set? It is simple, the value used to delineate the set!

A literal type "Hi" can only contain the value "Hi". That is, if the type "Hi" is seen as a set, it can only contain the value "Hi" as its element. 

The same applies to literal types made from numbers and booleans. So if we have a literal type of 42, it can only contain the value 42. If we have a literal type of true, it can only contain the value true. 

We can demonstrate this in TypeScript code as follow:
let greet: "Hi" = "Hi";
let answer: 42 = 42;
let justDoIt: true = true;
We can also use the type keyword, for example:
type TypeHi = "Hi";
type Type42 = 42;
type TypeBool = true;

let greet: TypeHi = "Hi";
let answer: Type42 = 42;
let justDoIt: TypeBool = true;
In above, the variable greet is annotated with the literal type "Hi", hence only the value "Hi" can be assigned to it. answer variable is typed as 42, hence only 42 can be assigned to it, while justDoIt is given the literal type true, which means only true value can be assigned to it. Trying to assign other values, apart from the values depicted by the literal type will lead to compile error. For example:
Type '"Hello"' is not assignable to type '"Hi"'.
This literal type can also be used for function arguments. For example, when used together in combination with [union]( types:

function doDelete(confirmation: "yes" | "no" | true | false | 1 | 0) {
 // implementation   
And the function can be called by values of the defined literal type:

But when attempted to be called by a literal value not defined in the union type, you get a compilation error:
leads to the compilation error:
Argument of type '"Ye"' is not assignable to parameter of type 'boolean | 0 | "yes" | "no" | 1'
Seen another way, literal types allow us to create types that can only contain one literal value. What then, is the type of such a restrictive type? Well, the literal value it contains!

Now that we have a good overview of literal types, let’s now look at Template Literal Types.

What are Template literal types?

The first thing to know about template literal types is that they are an extension of only literal types created from string literals.

Template literal types allow for the creation of string literal types by not only specifying literal strings but also by using string templates.

How does this look? To explain we first look at Template literals in JavaScript.

With the addition of Template literals, (previously called Template strings) in JavaScript, it became possible to have string interpolation. Where a string value can be constructed from the composition of variables together with other variables or literal strings. This is done via back-ticks. For example:
let name = "John Doe"
let hiGreetings = `Hi ${name}`
let helloGreetings = `Hello ${name}`

console.log(hiGreetings) // Hi John Doe
console.log(helloGreetings) // Hello John Doe
TypeScript, with Template Literal Types, allows this sort of combination when creating Literal Types based on string literal values. For example:
type Sun = "sun";
type Flower = "flower";

type Sunflower = `${Sun}${Flower}`

let sunFlower: Sunflower = "sunflower";
Here the Sunflower types are composed of two string literal types Sun and Flower It is also possible to combine string literal types with ordinary types when using template literal types. The following code snippet shows how that looks like:
type Pixels = `${number}px`

let width: Pixels = "20px"
Here the Pixels type is constructed from the combination of number which is just a type, and "px" which is a string literal type. It is worth mentioning here that literal types are subtypes of their primitive types. What does this mean? It means a value that is assignable to a literal type will also be assignable to the primitive type from which the literal type is created. That is:
let greet: "Hi" = "Hi";
let answer: 42 = 42;
let justDoIt: true = true;

let superGreet: string = greet;
let superAnswer: number = answer;
let superJustDoIt: boolean = justDoIt;
greet is of literal type "Hi". Since string is the primitive type from which the literal type is created, greet can also be assigned to superGreet which is a variable of type string. The same can also be seen for number and boolean. A consequence of this is that when a literal value is assigned to a variable without specifying the literal type. TypeScript will infer the type to be that of the primitive type. 

For example
let greet: "Hi" = "Hi";
let answer: 42 = 42;
let justDoIt: true = true;

let superGreet = greet;
let superAnswer = answer;
let superJustDoIt = justDoIt;

console.log(typeof superGreet) // prints "string"
console.log(typeof superAnswer) // prints "number"
console.log(typeof superJustDoIt) // prints "boolean"
One repercussion of this is that if a function is defined to take in values of literal type, that function cannot be called with a variable, even if the variable contains the literal value. For example, this is okay:
function greeter(salutation: "Hi", name: string) {
    console.log(salutation + " " + name)

let guest = "John"

greeter("Hi", guest)

While this won’t work even though the salute variable, contains the needed value:
let salute = "Hi"
let guest = "John"

greeter(salute, guest)
It will fail with the following compilation error:

Argument of type string is not assignable to parameter of type '"Hi"' 

This is because salute is now a variable of type string while the function expects a value of literal type "Hi". One way to work around this is not to leave the type inference to the compiler but to specifically type the variable salute of type "Hi". Even though this works, it feels quite cumbersome. 

Another way which feels less cumbersome is to define variables that are meant to contain values of literal type as const. Doing this enables the TypeScript compiler to properly infer the required type. For example the above can be made to work as follows:

function greeter(salutation: "Hi", name: string) {
    console.log(salutation + " " + name)

const salute = "Hi"
let guest = "John"

greeter(salute, guest)
So now that we have a good overview of literal types and template literal types in TypeScript, the next question one might ponder is: how can these features be put to use? The next post explores some ways literal and template literal types can be put to use.

This post is part of a series. See Introduction to Advance Types in TypeScript for more posts in the series.

No comments: