I talked briefly in Chapter 1 about the existence of a “type system” in TypeScript, where its type checker looks at your code, understands how it’s meant to work, and lets you know where you might have messed up. But how does a type system work, really?
A “type” is a description of what a JavaScript value shape might be.
By shape I mean how a value looks and feels: its properties and methods, and what the built-in typeof
operator would describe it as.
The most basic types in TypeScript correspond to the seven basic primitives in JavaScript:
bigint
: 0n
, 2n
, -4n
, …
boolean
: true
or false
null
number
: 0
, 2
, -4
, …
string
: ""
, "Hi!"
, `"ߑ뢠, …
symbol
: Symbol()
, Symbol("hi")
, …
undefined
For example, when you create a variable:
let
singer
=
"Aretha"
;
TypeScript can figure out, or infer, that the singer
variable is of type string
.
For each of these values, TypeScript understands the type of the value to be one of the seven basic primitives:
1337
n
;
// bigint
true
;
// boolean
null
;
// null
1337
;
// number
"Louise"
;
// string
Symbol
(
"Franklin"
);
// Symbol
undefined
;
// undefined
TypeScript is also smart enough to figure out the type of a variable whose starting value is computed.
In this example, TypeScript knows that the ternary expression always results in a string, so the bestSong
value is a string
:
let
bestSong
=
Math
.
random
()
>
0.5
?
"Chain of Fools"
:
"Respect"
;
At its core, TypeScript’s type system works by:
Reading in your code and understanding all the types and values in existence
For each object, seeing what type its initial declaration indicates it may contain
For each object, seeing all ways it’s used later on
Complaining to the user if an object’s usage doesn’t match with its type
Let’s walk through that type inference system in detail.
Take the following snippet, in which TypeScript is emitting a type error about a member variable being erroneously called as a function:
let
firstName
=
"Cleopatra"
;
firstName
.
length
();
// ~~~~~~
// This expression is not callable.
// Type 'Number' has no call signatures
TypeScript came to that complaint by, in order:
Reading in the code and understanding there to be one object: firstName
Concluding that firstName
is of type string
its initial value is a string
, "Cleopatra"
Seeing that the code is trying to access a .length
member of firstName
and call it like a function
Complaining that the .length
member of a string is a number, not a function (it can’t be called like a function)
Understanding TypeScript’s type inference is an important skill for understanding TypeScript code. Code snippets in this chapter and through the rest of this book will display more and more complex types that TypeScript will be able to infer from code.
While writing TypeScript, the two kinds of “errors” you’ll come across most frequently are:
Syntax: blocking TypeScript from being converted to JavaScript.
Type: something mismatched has been detected by the type checker.
It’s useful to be able to understand the differences between the two.
Syntax errors are when TypeScript detects incorrect syntax that it cannot understand as code. These block TypeScript from being able to properly generate output JavaScript from your file. Depending on the tooling and settings you’re using to convert your TypeScript code to JavaScript, you might to still get some kind of JavaScript output. If you do, it likely won’t look like what you expect.
This input TypeScript has a syntax error for an unexpected let
:
let
let
wat
;
Its compiled output in TypeScript, depending on the language version, may look something like:
let
let
,
wat
;
Type errors occur when your syntax is valid but the TypeScript type checker has detected an error with the program’s types. These do not block TypeScript syntax from being converted to JavaScript. They do, however, often indicate something will crash or behave unexpectedly if your code is allowed to run.
You saw this earlier with the console.blub
example, where it is syntactically valid code but TypeScript can detect it will likely crash when run:
console
.
blub
(
"Hello world!"
);
Some projects are configured to block running code during development until all TypeScript type errors -not just syntax- are fixed.
Many developers, myself included, generally find this to be annoying and unnecessary.
Most projects have a way to disable it, such as with the tsconfig.json
file covered in Chapter ??.
TypeScript reads variables’ initial values to determine what type those variables are allowed to be. If it later sees an assignment of a new value to that variable, it will check if that assignment value’s type is the same as the variable’s.
TypeScript would be fine with later assigning a different value of the same type to a variable. If a variable is, say, initially a string value, later assigning it another string would be fine:
let
firstName
=
"Carole"
;
firstName
=
"Louise"
;
When a variable is declared with an initial value, it’s considered to be of that initial value’s type — and must always be assigned values of that type. If TypeScript sees an assignment of a different type, it will give us type error. We couldn’t, say, initially declare a variable with a string value and then later on put in a number:
let
firstName
=
"King"
;
firstName
=
1337
;
// Error: Type 'number' is not assignable to type 'string'.
TypeScript’s checking of whether a value is allowed to be provided to a function call or variable is called “assignability”: whether that value is assignable to the location it’s passed to. This will be an important term in later chapters as we compare more complex objects.
Sometimes a variable doesn’t have an initial value for TypeScript to read.
TypeScript won’t attempt to figure out what value those variables are from later usage.
It’ll consider them to be implicitly the any
type: a type indicating that it could be anything in the world.
let
rocker
;
rocker
=
"Joan Jett"
;
Allowing variables to be of type any
defeats the purpose of TypeScript’s type checking!
TypeScript works best when it knows what types your values are meant to be.
Most of TypeScript’s type checking can’t be applied to any
typed values because they don’t have known types to be checked.
Although TypeScript can still emit JavaScript code despite an implicit any
, it will yell at you about them in the form of type errors.
Chapter ?? will cover how to configure TypeScript’s implicit any
complaints.
Instead, TypeScript provides a syntax for declaring the type of a variable, using what’s called a type annotation. A type annotation is placed after the name of a variable and includes a colon followed by the name of a type.
let
rocker
:string
;
rocker
=
"Joan Jett"
;
These type annotations are unique to TypeScript.
If you run tsc
to compile TypeScript source code to JavaScript, they’ll be erased.
For example:
// output .js file
let
rocker
;
rocker
=
"Joan Jett"
;
TypeScript contains an assortment of new pieces of syntax such as these type annotations that exist only in the type system. Nothing that exists only in the type system gets copied over into emitted JavaScript.
Type annotations allow us to provide information to TypeScript that it wouldn’t have been able to glean on its own. You could use them on variables that have immediately inferable types, but you wouldn’t be telling TypeScript anything it doesn’t already know:
let
firstName
:string
=
"Tina"
;
// ~~~~~~~~ Does not change the type system...
If you do add a type annotation to a variable with an initial value, TypeScript will check that it matches the type of the variable’s value.
The following firstName
is declared to be of type string
but its initializer is the number
42
, which is TypeScript sees as an incompatibility:
let
firstName
:string
=
42
;
// ~~~~~~~~~
// Error: Type 'number' is not assignable to type 'string'.
Most developers -myself included- generally prefer not to add type annotations in places where they don’t change anything. Having to manually write out type annotations can be cumbersome — especially when they change, and for the complex types I’ll show you later in this book.
TypeScript doesn’t only check that the values assigned to variables match their original types: it also knows what member properties should exist on objects. If you attempt to access a property of a variable, TypeScript will make sure that property is known to exist on that variable’s type.
Suppose we declare a rapper
variable of type string
.
Later on, when we use that string
variable, operations that TypeScript knows work on strings are allowed:
let
rapper
=
"Queen Latifah"
;
rapper
.
length
;
// ok
Operations that TypeScript doesn’t know to work on strings will not be allowed:
rapper
.
asdfqwerty
;
// ~~~~~~~~~~
// Property 'asdfqwerty' does not exist on type 'string'.
Types can also be more advanced shapes, most notably complex objects.
In the following snippet, TypeScript knows the birthNames
object doesn’t have a rihanna
key and complains:
let
cher
=
{
firstName
:
"Cherilyn"
,
lastName
:
"Sarkisian"
,
};
cher
.
middleName
;
// ~~~~~~~~~~
// Property 'middleName' does not exist on type
// '{ firstName: string; lastName: string; }'.
Chapter 7 will describe more on objects and object types.
In this chapter, you saw how TypeScript’s type system works at its core:
What a “type” is and what types are recognized by TypeScript
Inferred variable types and variable assignability
Type annotations to explicitly declare variable types
Object member checking on type shapes
How type complaints compare to syntax complaints
3.85.224.214