Chapter 4. Literals

Chapter 3 introduced the concept of union types and narrowing, allowing you to work with values that may be two or more potential types. I’ll now go the opposite direction by introducing literal types: more specific versions of primitive types.

Literal Types

Take this philosopher variable:

const philosopher = "Hypatia";

What type is `philosopher?

At first glance, you might say string — and you’d be correct. philosopher is indeed a string.

But! philosopher is not just any old string. It’s specifically the value "Hypatia". Therefore, the philosopher variable’s type is technically "Hypatia" itself as well.

Such is the concept of a literal: a value that is known to be a distinct instance of a primitive.

If you declare a variable as const and directly give it a literal value, TypeScript will understand the variable to be that literal value as a type. This is why, when you hover a mouse over a const variable in an IDE such as VS Code, it will show you the variable’s type as that literal (Figure 4-1) instead of the more general primitive (Figure 4-2).

TypeScript reporting a 'const' variable as being its string literal type.
Figure 4-1. TypeScript reporting a const variable as being specifically its literal type.
TypeScript reporting a 'let' variable as being type 'string'.
Figure 4-2. TypeScript reporting a let variable as being generally its primitive type.

You can think of each primitive type as a union of every possible matching literal value. Most primitive types have a theoretically infinite number of literal types, but not all. Out of some common ones you’ll find in typical TypeScript code:

  • boolean: Just true | false

  • null and undefined: both just have one literal value, themselves

  • number: 0 | 1 | 2 | ... | 0.1 | 0.2 | ...

  • string: "" | "a" | "b" | "c" | ... | "aa" | "ab" | "ac" | ...

Union type annotations can mix and match between literals and primitives. A representation of a lifespan, for example, might be represented by any number or one of a couple known edge cases:

let lifespan: number | "ongoing" | "unknown";

lifespan = 89; // Ok
lifespan = "ongoing"; // Ok

lifespan = true;
// Error: Type 'true' is not assignable to
// type 'number | "ongoing" | "unknown"'

Literal Assignability

Literal types are subsets of their general primitive types. A value that is known to be a different literal, or only as a general primitive, may not be assigned to a variable that only allows a different literal.

In this example, specificallyAda is declared as being of the literal type "Ada", so the types "?!" and string are not assignable to it:

let specificallyAda: "Ada";

specificallyAda = "Ada"; // Ok

specificallyAda = "?!";
// Error: Type '"?!"' is not assignable to type '"Ada"'.

let someString = "";

specificallyAda = someString;
// Error: Type 'string' is not assignable to type '"Ada"'.

Literal types are, however, allowed to be assigned to their corresponding primitive types. A specific literal string is still generally a string, after all.

In this code example, the value ":)", which is of type ":)", is being assigned to the someString variable previously inferred to be of type string.

someString = ":)";

Who would have thought a simple variable assignment would be so theoretically intense?

Strict Null Checking

The power of narrowed unions with literals is particularly visible when working with potentially undefined valuables, an area of type systems known as strict null checking. TypeScript is part of a surge of modern programming languages that utilizes strict null checking to fix the dreaded “billion dollar mistake”.

The Billion Dollar Mistake

The “billion dollar mistake” is a catchy industry term for many type systems allowing values such as null to be used in places that require a different type. In languages without strict null checking, code like example that assign null to a string is allowed:

const firstName: string = null;

If you’ve previously worked in a typed language such as C++ or Java that suffers from the billion dollar mistake, it may be surprising to you that some languages don’t allow such a thing. If you’re never worked in a language with the billion dollar mistake before, it may be surprising that some languages do allow such a thing in the first place!

In the words of the developer who coined the phrase:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965… This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Tony Hoare, 2009

The TypeScript compiler has an --strictNullChecks option to toggle whether strict null checking is enabled. With the option disabled, the following code is considered totally type safe:

let nameMaybe = Math.random()
    ? "Tony Hoare"
    : undefined;

nameMaybe.toLowerCase();

Roughly speaking, disabling strict null checking adds | null | undefined to every type in your code, thereby allowing any variable to receive null or undefined.

TypeScript best practice is generally to enable strict null checking. Doing so helps prevent crashes and eliminates the billion dollar mistake.

With strict null checking enabled, TypeScript sees the potential crash in the code snippet:

let nameMaybe = Math.random()
    ? "Tony Hoare"
    : undefined;

nameMaybe.toLowerCase();
// Error: Object is possibly 'undefined'.

Truthiness Narrowing

TypeScript can also narrow a variable’s type from a truthiness check if only some of its potential values may be truthy. In the following snippet, cytogeneticist is of type string | undefined, and because undefined is always falsy, TypeScript can deduce that it must be of type string within the if statement’s body:

let cytogeneticist = Math.random() > 0.5
    ? "Barbara McClintock"
    : undefined;

if (cytogeneticist) {
    cytogeneticist.toUpperCase(); // Ok: string
}

cytogeneticist.toUpperCase();
// Error: Object is possibly 'undefined'.

Logical operators that perform truthiness checking work as well, namely ?. and &&:

cytogeneticist.toUpperCase(); // Ok: string
cytogeneticist && cytogeneticist.toUpperCase(); // Ok: string

Unfortunately, truthiness checking doesn’t go the other way for primitive values. If all we know about a string | undefined value is that it’s falsy, that doesn’t tell us whether it’s "" or undefined.

Here, placeholder is of type false | string, and while it can be narrowed down to just string in the if statement body, the else statement body knows it can still be a string if it’s "":

let biologist = Math.random() > 0.5 && "Rachel Carson";

if (!biologist) {
    biologist.toUpperCase(); // Not ok: type `false | string`
           // ~~~~~~~~~~~
           // Error: Property 'toUpperCase' does not
           // exist on type 'string | false'.
           //   Property 'toUpperCase' does not
           //   exist on type 'false'.
}

Implicit Union Type Truthiness

If you declare a variable with a type and no initial value, TypeScript is smart enough to understand that the variable is undefined until a value is assigned. It will even give you a specialized error message letting you know if you try to access a member before assigning a value:

let mathematician: string;

mathematician.length;
// Error: Variable 'mathematician' is used before being assigned.

mathematician = "Mark Goldberg";
mathematician.length; // Ok

Summary

In this chapter, you saw how TypeScript handles specific “literal” values within primitives:

  • The difference between const literals and let primitives

  • The “Billion Dollar Mistake” and how TypeScript handles strict null checking

  • Using explicit | undefined to represent values that might not exist

  • Implicit | undefined for unassigned variables

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.0.192