Chapter 2. The Type System

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?

What’s in a Type?

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:

1337n; // 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";

Type Inferences in Detail

At its core, TypeScript’s type system works by:

  1. Reading in your code and understanding all the types and values in existence

  2. For each object, seeing what type its initial declaration indicates it may contain

  3. For each object, seeing all ways it’s used later on

  4. 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:

  1. Reading in the code and understanding there to be one object: firstName

  2. Concluding that firstName is of type string its initial value is a string, "Cleopatra"

  3. Seeing that the code is trying to access a .length member of firstName and call it like a function

  4. 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.

Kinds of Errors

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

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

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!");
Example 2-1.

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 ??.

Assignability

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.

Type Annotations

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";
Example 2-2.

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.

Unnecessary Type Annotations

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'.
Example 2-3.

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.

Type Shapes

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.

Summary

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

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

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