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.
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).
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 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?
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” 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'.
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'.
}
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
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
18.227.0.192