[ui.dev] Learn Typescript - part 1

Introduction, Philosophy, and Tips

Welcome! (Don’t skip this)

Learning anything new is always a significant endeavor. I don’t take for granted that there’s an opportunity cost in taking this course. Trust that every aspect of this course, from the text and videos to the quizzes and curriculum have been meticulously thought over and debated. The end result is what we strongly believe is the most in-depth and effective way to learn TypeScript that exists.

Before starting this course, you should have a good understanding of JavaScript. You’ll have the best learning experience if you’ve already completed the Modern JavaScript and Advanced JavaScript courses. That said, we’ll be going back to some basic JavaScript principles and looking at them in the context of TypeScript.

Before we dive into the material, there are some important housekeeping items to mention first.

  1. I believe very strongly in creating a linear approach to learning. You should feel as if you’re walking up a staircase, not rock climbing. Logistically what this means is that this course is very in-depth. At times you may be tempted to skip certain texts or videos; don’t. Because of this linear approach, each new section builds upon the previous sections. If you’re already familiar with a certain topic, skim it, don’t skip it.
  2. This course is designed to teach the fundamentals of TypeScript, but it doesn’t cover any specifics about using TypeScript with frameworks, such as React. We’re preparing a course that will cover TypeScript in React in depth, but if you complete the course, you should have enough familiarity to learn how to use TypeScript with other frameworks on your own.
  3. Throughout the course, you’ll see various types of content. Here’s a breakdown.
  • The first introduction you’ll see to any new concept will be in a blog post/video format. This ensures you’re able to hyperfocus on the new concept without needing any external context. If you see a section without the () tags (listed below), it’s this format. You’re not expected to code along during these sections.
  • (Project) - At the end of the course, we’ll be building a project together over the course of several videos. Above each (Project) video will be a link to its coinciding branch as well as the specific commit on Github. If you get stuck or your code isn’t working, check your code against the code in the commit. You are expected to code along with these sections.
  • (Practice) - These are usually hosted on Code Sandbox and are designed to get you experience with a particular topic. These will typically involve us converting existing JavaScript code to TypeScript. You might be tempted to skip these; don’t.
  • (Solution) - These are the solution videos to the (Practice)s.
  • (Quiz) - Your typical quiz. If you find yourself guessing on the quiz problems, redo the section. If the quizzes seem “easy”, that’s a good sign.
  • (Bonus) - Bonus material that doesn’t fit into the flow of the normal course but is still important to know.
  1. If you skipped #3, go read it.
  2. Once the course is over, you’ll be given a link to the curriculum. This will be a project that you’ll be expected to build yourself. This will be the hardest part of the course but also the most rewarding.
  3. You can take this course regardless of if you’re on Mac, Linux, or Windows. If you’re wondering, I’m running Node v12.18.3, TypeScript 4, Visual Studio Code, and Google Chrome 85. I recommend using Visual Studio Code, since it has TypeScript support built in, but if you want to use a different code editor, there are TypeScript plugins available for most major code editors.
  4. If you haven’t already, visit our official community for subscribers. It’s where other members of the team and I will be hanging out answering support questions. Again, if your code isn’t working after following a video, very carefully compare it to the specific commit for that video before asking a question in the community.
  5. If you run into any errors/typos/etc. related to the course material, you can make a comment on this issue and I’ll get it fixed.

Good luck!

Projects (What you’ll build)

You will build two projects during this course. The first is a Tic Tac Toe game built with vanilla DOM APIs and will happen mid-way through the course.

The second is a very simple webserver built with NodeJS. This will let you practice configuring TypeScript for NodeJS development.

The curriculum will be Hacker News clone that you will build entirely on your own. I built it using vanilla DOM APIs, but if you want to use a front-end framework like React or Vue, or build it as a NodeJS web server, you are more than welcome to. Be aware that front-end frameworks have their own type definitions that we don’t cover in this course which you will have to work with.

(Pre-req) Glossary

Throughout the course, we’ll be using some terms that you might not have heard.

  • Compile Time vs Runtime : We consider the time before your code is executed to be “Compile Time”. Anything that happens in your program after you execute your code happens at runtime.
  • Dynamic Types vs Static Types : Languages like Java and C++ expect variables to always be the same type after they are declared. They use static, or unchanging, types. Other languages, like JavaScript and Python, let you change the type of variables at runtime. These use dynamic types.
  • Type Error : When the program throws an error because a value of one type was used when the program was expecting another type. This happens when we use numbers in place of strings or if we try to call a function, only to find out it is really undefined .
  • Type Safety : Eliminating type errors by ensuring types are only used in the correct place. This usually happens by having a compiler warn you when you use a type in the wrong way. There are varying degrees of type safety, from incredibly rigid with a low chance of type errors to more flexible with a higher chance of type errors.
  • Static Analysis or Static Validation : When a tool analyzes your code without executing it. This can give you some insights into ways to improve your code and can warn you of errors before you run your code.
  • Compiler : A program which takes code and transforms it into another format. Typically compilers convert code into machine code which is run directly by the CPU, but other compilers, like TypeScript, convert from one programming language into another programming language.
  • Type Annotations : Small bits of code that tell TypeScript what type a value or variable is.
  • Type Declaration : A file with the .d.ts extension which only holds type definitions for a JavaScript library.

Why TypeScript?

Types are foundational to JavaScript; they influence everything about the way you write your programs. But JavaScript’s dynamic type system often makes it easy to introduce bugs by mistakenly referencing variables with the wrong type. How many of us have experienced the dreaded Undefined is not an object error?

TypeScript was invented to stop these problems from happening before you even run your code. It uses JavaScript as a base, adds some extra syntax to define types, and provides a tool to transform your code back into JavaScript. The result is a hybrid of a compiler and a linter. TypeScript checks your code as you write it to make sure the types are correct, and then compiles it to whatever version of JavaScript you need to support.

TypeScript is superset of JavaScript, meaning that any JavaScript program is also a valid TypeScript program. This allows TypeScript programs to import and use JavaScript code, meaning you don’t have to rewrite your entire app right away when you start using TypeScript. Other developer tools, like Babel or Webpack, can be configured to process TypeScript files, and new JavaScript runtimes like Deno support TypeScript natively.

There’s a reason TypeScript is so popular. Developers who use it love the type safety it provides while still giving them the flexibility of JavaScript. Here are some reasons why you might find TypeScript useful.

Type Validation

TypeScript is a static validation tool, like ESLint. The goal of any kind of validation tool is to increase your confidence that the program you are writing behaves the way it is supposed to and doesn’t have unexpected bugs. Since TypeScript understands the relationships between the types in your codebase, using it well can eliminate entire classes of errors and issues. As you write your code, TypeScript can give you hints to help you write your code correctly. TypeScript doesn’t replace ESLint or automated tests, but it can give you more confidence that your code is working correctly without having to run your code.

The thing I :heart: about @TypeScript is that when you run your code, you do it to see if it works as intended, not to see if it works.

— donavon west (@donavon) May 15, 2020

Compiler

TypeScript is also a compiler. Like Babel, it can transform TypeScript and JavaScript code to support different features for older JavaScript engines. If you need to support an older browser like IE11, TypeScript can transform your code so it will work correctly. If you only need to support newer engines, the compiler will strip out the type data and leave you with valid JavaScript.

It’s important to remember that TypeScript is not a runtime language - all of the types are removed at compile time. It’s purpose is to encourage you, the developer, to use the types to add checks to your program so you have fewer errors and bugs.

JavaScript Superset

TypeScript can be used as a progressive enhancement to JavaScript. It’s a superset of JavaScript, so any JavaScript program is valid TypeScript. This includes the massive amount of packages available on NPM. However, without proper type annotations, most JavaScript programs will have type errors. You can configure the compiler to ignore type errors on JavaScript programs as you transition your codebase from JavaScript to TypeScript.

Since TypeScript is a superset of JavaScript, many of the quirks of JavaScript are present in TypeScript programs. It might not always be possible to add the appropriate type annotations to something. TypeScript provides escape hatches, like the any type and // @ts-ignore which turn off the type checker for those parts of your code. This is just like writing regular JavaScript, so you can still enjoy the flexibility of dynamic types. However, it also means you can’t guarantee that your program will never have a type error. If you want a truly strongly typed language that compiles to JavaScript, you can use languages like ReScript or Elm.

Communication and Documentation

Coming into a brand new codebase can be daunting, especially if you don’t know how all the pieces connect together or how something is supposed to behave. Adding type annotations to your code removes a lot of the guess work in figuring out how a program works and serves as guide for anyone trying to work in your codebase.

TypeScript can also help with refactoring. Without TypeScript, you have no way of knowing all of the different places that need to be updated when you change something. TypeScript checks your change against any other files that might be affected and warns you if anything is broken. All that’s left is to follow each warning and make the appropriate change without needing to run your code.

In short, TypeScript will help you write code with less errors by checking your code before you run it. Adding the types might take a little bit of work, but it definitely pays off in the long run.

(Bonus) ECMAScript, TC39, and the Standardization process

This is originally part of our Modern JavaScript course. However, it’s applicable to us here as well.

JavaScript is a living language that is constantly adding new features. As a JavaScript developer, it’s important to understand the underlying process that’s needed to take a new feature and transform it from a simple idea, to part of official language specification. To do that, we’ll cover three topics - Ecma, EcmaScript, and the TC39.

First, let’s take ourselves back to 1995. The cult classic Heavy weights was in theaters, Nicolas Cage won an Oscar, and websites typically looked something like this. Now, odds are the way you would view that website would be with Netscape Navigator. At the time, Netscape Navigator was the most popular web browser with almost 80% market share. The founder of Netscape, the company behind Netscape Navigator, was Marc Andreessen. He has a vision for the future of the web and it was more than just a way to share and distribute documents. He envisioned a more dynamic platform with client side interactivity - a sort of “glue language” that was easy to use by both designers and developers. This is where Brendan Eich comes into the picture.

Brendan was recruited by Netscape with the goal of embedding the Scheme programming language into Netscape Navigator. However, before he could get started, Netscape collaborated with Sun Microsystems to make their up and coming programming language Java available in the browser. Now, if Java was already a suitable language, why bring on Brendan to create another one?. If you remember back to Netscape’s goal, they wanted “a scripting language that was simple enough for designers and amateurs to use” - sadly, Java wasn’t that. From there, the idea became that Java could be used by “professionals” and this new language “Mocha” (which was the initial name of JavaScript) would be used by everyone else. Because of this collaboration between languages, Netscape decided that Mocha needed to compliment Java and should have a relatively similar syntax.

From there, as the legend states, in just 10 days Brendan created the first version of Mocha. This version had some functionality from Scheme, the object orientation of SmallTalk, and because of the collaboration, the syntax of Java. Eventually the name Mocha changed to LiveScript then LiveScript changed to JavaScript as a marketing ploy to ride the hype of Java. So at this point, JavaScript was marketed as a scripting language for the browser - accessible to both amateurs and designers while Java was the professional tool for building rich web components.

Now, it’s important to understand the context of when these events were happening. Besides Nicolas Cage winning an Oscar, Microsoft was also working on Internet Explorer. Because JavaScript fundamentally changed the user experience of the web, if you were a competing browser, since there was no JavaScript specification, you had no choice but to come up with your own implementation. As history shows, that’s exactly what Microsoft did and they called it JScript.

This then lead to a pretty famous problem in the history of the internet. JScript filled the same use case as JavaScript, but its implementation was different. This meant that you couldn’t build one website and expect it to work on both Internet Explorer and Netscape Navigator. In fact, the two implementations were so different that “Best viewed in Netscape” and “Best viewed in Internet Explorer” badges became common for most companies who couldn’t afford to build for both implementations. Finally, this is where Ecma comes into the picture.

Ecma International is “an industry association founded in 1961, dedicated to the standardization of information and communication systems”. In November of 1996, Netscape submitted JavaScript to Ecma to build out a standard specification. By doing this, it gave other implementors a voice in the evolution of the language and, ideally, it would help keep other implementations consistent across browsers.

Under Ecma, each new specification comes with a standard and a committee. In JavaScript’s case, the standard is ECMA-262 and the committee who works on the ECMA-262 standard is the TC39. If you look up the ECMA262 standard, you’ll notice that the term “JavaScript” is never used. Instead, they use the term “EcmaScript” to talk about the official language. The reason for this is because Oracle owns the trademark for the term “JavaScript”. To avoid legal issues, Ecma decided to use the term EcmaScript instead. In the real world, ECMAScript is usually used to refer to the official standard, ECMA-262, while JavaScript is used when talking about the language in practice. As mentioned earlier, the committee which oversees the evolution of the Ecma262 standard is the TC39, which stands for Technical Committee 39. The TC39 is made up of “members” who are typically browser vendors and large companies who’ve invested heavily in the web like Facebook and PayPal. To attend the meetings, “members” (again, large companies and browser vendors) will send “delegates” to represent said company or browser. It’s these delegates who are responsible for creating, approving, or denying language proposals.

When a new proposal is created, that proposal has to go through certain stages before it becomes part of the official specification. It’s important to keep in mind that in order for any proposal to move from one stage to another, a consensus among the TC39 must be met. This means that a large majority must agree while nobody strongly disagrees enough to veto a specific proposal.

Each new proposal starts off at Stage 0. This stage is called the “Straw man” stage. Stage 0 proposals are “proposals which are planned to be presented to the committee by a TC39 champion or, have been presented to the committee and not rejected definitively, but have not yet achieved any of the criteria to get into stage 1.” So the only requirement for becoming a Stage 0 proposal is that the document must be reviewed at a TC39 meeting. It’s important to note that using a Stage 0 feature in your codebase is fine, but even if it does continue on to become part of the official spec, it’ll almost certainly go through a few iterations before then.

The next stage in the maturity of a new proposal is Stage 1. In order to progress to Stage 1, an official “champion” who is part of TC39 must be identified and is responsible for the proposal. In addition, the proposal needs to describe the problem it solves, have illustrative examples of usage, a high level API, and identify any potential concerns and implementation challenges. By accepting a proposal for stage 1, the committee signals they’re willing to spend resources to look into the proposal in more depth.

The next stage is Stage 2. At this point, it’s more than likely that this feature will eventually become part of the official specification. In order to make it to stage 2, the proposal must, in formal language, have a description of the syntax and semantics of the new feature. In other words, a draft, or a first version of what will be in the official specification is written. This is the stage to really lock down all aspects of the feature. Future changes may still likely occur, but they should only be minor, incremental changes.

Next up is Stage 3. At this point the proposal is mostly finished and now it just needs feedback from implementors and users to progress further. In order to progress to Stage 3, the spec text should be finished and at least two spec compliant implementations must be created.

The last stage is Stage 4. At this point, the proposal is ready to be included in the official specification. To get to Stage 4, tests have to be written, two spec compliant implementations should pass those tests, members should have significant practical experience with the new feature, and the EcmaScript spec editor must sign off on the spec text. Basically once a proposal makes it to stage 4, it’s ready to stop being a proposal and make its way into the official specification. This brings up the last thing you need to know about this whole process and that is TC39s release schedule.

As of 2016, a new version of ECMAScript is released every year with whatever features are ready at that time. What that means is that any Stage 4 proposals that exist when a new release happens, will be included in the release for that year. Because of this yearly release cycle, new features should be much more incremental and easier to adopt.

JavaScript Types

Stick with me here because this section is going to get a little dense. However, it’s necessary to set a solid foundation of the JavaScript type system before jumping into TypeScript.

All programming languages use types of some kind to know how to represent and operate on the bits of code that pass through the processor as the program is executed. The types and type operators for JavaScript are the same in TypeScript, so we’ll go over each of them to make sure we’re familiar.

When comparing types in JavaScript, it can be helpful to use the typeof operator. When placed in front of a value, it gives you a string representation of that value’s type.

typeof true; // "boolean"

let fruitName = "Banana";
typeof fruitName; // "string"

There are six primitive data types in JavaScript: Boolean , Number , BigInt , String , Symbol , and undefined . A primitive type represents a single specific type of value and is immutable, meaning we can’t directly modify its value, we can only reassign a different value to a variable. Let’s go over each of them individually.

Boolean

Boolean s represent values that are either true or false . These are used for conditionals and logical operations using the AND ( && ), OR ( || ) and NOT ( ! ) operators.

In JavaScript, some values of different types are considered “falsy”, which means if you were to cast the value into a Boolean using two NOT operators or with the Boolean() built-in function, it would become false , where every other value would become true . I’ll point out the “falsy” values for each type.

Number

Number s in JavaScript are stored as floats, which means you use the same type for whole integers and decimals. JavaScript also has values for Infinity , -Infinity , and NaN , or “Not a Number”.

Both 0 and NaN are “falsy” values. NaN is a little confusing. It’s used to represent values that shouldn’t exist or are imaginary, like Math.sqrt(-1) . Even though NaN literally means “Not a Number”, it’s type is still Number . You also can’t check if a NaN value is equal to NaN

let imaginaryNumber = Math.sqrt(-1); // NaN

typeof imaginaryNumber; // "number"
imaginaryNumber === NaN; // false

To check if a Number is NaN , you can use the Number.isNaN() method.

Number.isNaN(imaginaryNumber); // true

BigInt

Since Number s are limited in how big they can be, JavaScript has a separate type for very large integers. BigInt s were introduce to JavaScript in ES2019 and are created by appending n to the end of a Number integer.

typeof 1337n; // "bigint"
1337n === 1337; // false

The BigInt value 0n is “falsy”

String

String s store text data. You can create string literals with single quotes (’), double quotes ("), and backticks (). Strings in JavaScript are immutable, which means once a string is created, you cannot mutate it to change it. An empty string (""` ) is the only falsy string value.

Symbol

Symbol s are unique and immutable, which means each Symbol is not equal to any other Symbol . You can give a Symbol a description when you create it, but that is not required.

let symbol1 = Symbol("Apple");
let symbol2 = Symbol("Apple");

symbol1 === symbol2; // false

typeof symbol1; // "symbol"

undefined & null

undefined is its own types, is falsy. undefined represents uninitialized values, including properties of object s that don’t have a value.

null is an extra type used to represent the absence of value. Due to a bug in JavaScript, checking the type of null with typeof will return object . It’s kind of silly, but there are websites out there that expect this behavior. As Hyrum’s Law states, if there’s something wrong with the system, someone will come to depend on that flaw. Fortunately, you can compare null to itself.

typeof undefined; // "undefined"

let nullish = null;
typeof nullish; // "object"
nullish === null; //true

Bugs are often created when JavaScript tries to access object properties on a null or undefined value. Later on we’ll see how TypeScript can help us avoid those bugs.

JavaScript also has structural types, which means we can construct different shapes of the primitive types to represent more complex data structures.

Object

Object is the most versatile type in JavaScript. An Object can have any number of properties, each with its own value. This allows you to create unique and interesting data structures. Object s typically use String s for indexing the properties, but Symbol s can be used as well. A property’s value can be any type.

All of the other types we’ve looked at so far can be directly compared by value, but objects (and object-like values) are compared by reference. This means two objects that have the same shape aren’t considered equal.

let car = {
  wheels: 4,
  color: "red",
};
let car2 = {
  wheels: 4,
  color: "red",
};

car === car2; // false

let carClone = car;
car === carClone; // true

typeof car; // "object"

Array

Array s are technically the same type as Object s, but they are different enough that we’ll treat them as separate types. Array s store data, but use Number s instead of String s for indexing properties. This means that the data stored in Array s is ordered.

We can check if a value is an array using the Array.isArray() method.

let myArray = [1, 2, 3];

typeof myArray; // "object"
Array.isArray(myArray); // true

Function

Functions are defined blocks of code that you can call from elsewhere in your program. They can take parameters and have a return value. One of the best things about JavaScript is that functions can be treated as values. That means you can pass them between functions or use them whenever you want. That means that Function s are their own type as well.

typeof function () {}; // "function"
typeof (() => {}); // "function

Classes

Classes were introduced to JavaScript in ES2015 and serve as syntactic sugar around JavaScript’s prototypical inheritance system. We’ll talk more about classes later in the course, but for now we can look at the types that classes generate.

Classes can be defined with properties and given a constructor which initializes instances of that class. Within the class, we can use the special this variable to access properties on the class instance. Since the class constructor is what we call when creating a new instance of a class, the type of a class is Function . Instances of a class are Object s.

class Car {
  constructor(wheels, color) {
    this.wheels = wheels;
    this.color = color;
  }
}

typeof Car; // "function"

let motorcycle = new Car(2, "black");
typeof motorcycle; // "object"

If we want to see if an object is an instance of a specific class, we can use the instanceof operator.0

motorcycle instanceof Car; // true

We’ll look at classes more in-depth in a later section.

Basic TypeScript Configuration

To use TypeScript, we’ll need to install it globally with NPM.

npm install -g typescript

Once that’s done, we can use the tsc command to run the TypeScript compiler.

If we’ve installed TypeScript locally in our project, we can also run that version using npx

npx tsc

During this course, we’ll assume that TypeScript is globally installed.

TSConfig.json

The first command we’ll run will initialize our tsconfig.json file. We can use this file to tell TypeScript how we want it to run its compiler. We could also use a long list of command line flags, but that seems tedious and repetitive. To initialize the file, run

tsc --init

TSC is the TypeScript command line tool. We run this to type check and compile our code into JavaScript. If we have a tsconfig.json file in our project directory, tsc will pick up all of the configuration options we used in that file. We can override those options using command line flags. During this course, we’ll assume that all of our TypeScript code has an accompanying tsconfig.json file, which means all we need to do to run TypeScript is run tsc with no options.

When we open up the tsconfig.json file, it looks something like this. Don’t get overwhelmed, we don’t need to learn all of this at once. This is just to illustrate how configurable TypeScript is if we need to adjust something.

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */

    /* Basic Options */
    // "incremental": true,                   /* Enable incremental compilation */
    "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
    "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
    // "lib": [],                             /* Specify library files to be included in the compilation. */
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    // "outDir": "./",                        /* Redirect output structure to the directory. */
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true /* Enable all strict type-checking options. */,
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    // "noUnusedLocals": true,                /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */

    /* Advanced Options */
    "skipLibCheck": true /* Skip type checking of declaration files. */,
    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
  }
}

There are a lot of things that we can configure here! Fortunately, it’s given us some good defaults and explanations for all of the other settings. We’ll talk about many of these settings in a later lesson, but for now we’ll focus on the ones that aren’t commented out.

"target": "es5",

The target field lets us choose what version of JavaScript the compiler will transform our code into. This is useful if we want to target older browsers, like Internet Explorer 11. Targeting older browsers often means adding more code to your JavaScript files for polyfills and workarounds. Also, modern browsers are able to add optimizations for the newer syntax which can’t be replicated by compilers. There’s a bonus section later in the course which explains what each of the target options does to your compiled code.

"module": "commonjs",

The module field lets us change what module system the output of our bundle will use. This affects how our import and export statements are transformed in the final JavaScript output. If I were making an app for NodeJS or if I were using a bundler like Webpack, I would choose CommonJS.

"isolatedModules": true

This setting is commented out, but it’s important to know about. When this is on, TypeScript expects ever file to be a module. That means it has either an import or an export statement somewhere in the file. If we don’t have anything to import or export for some reason, we can turn a file into a module by adding export default {} to the end of the file. This setting is helpful when working with external tools, like Babel.

"strict": true,

Our tsconfig.json file lets us change how strict the type checker is when checking our types. There are a lot of options which we can choose from, but if we just want to enable all of them and have the type system catch as many bugs as possible, all we have to do is enable the strict flag. I’ll have it enabled for most of this course, and I encourage you to do the same. Here are some benefits you’ll get from using the strict flag.

  • noImplicitAny - any is a type in TypeScript which turns off the type checker for a variable and makes it act like regular JavaScript. If we want a variable to do that, we should be explicit about it so we don’t accidentally create type errors for ourselves. We’ll learn about the any type in the section on special TypeScript types.
  • strictNullChecks - With this on, TypeScript will automatically warn us any time a variable could be null or undefined . This will help us avoid the dreaded Cannot read property of undefined errors. We’ll cover this more in the section on narrowing types.
  • noImplicitThis - If we ever try to access the this variable on an object or class, TypeScript will warn us to make a type annotation so we know what properties we can expect to find on this . We’ll learn more about this in the section on classes.
  • strictPropertyInitialization - This flag warns us if any of our class properties aren’t initialized in the constructor. That would mean that the property is undefined, which could lead to unexpected bugs. We’ll look more into this in the section on classes.
`esModuleInterop`: true

This flag makes it much easier to use code from both the CommonJS and the ES Modules system. This is often necessary when working with packages from NPM. It adds a small bit of code to our JavaScript output, but saves a lot of headaches.

"skipLibCheck": true,

This flag tells TypeScript not to check all of the files that we are importing from the node_modules folder so the compiler and type checker can run faster. This means we have to trust that the type definitions for the packages we import are accurate, but if you want more confidence in your types at the expense of longer compilation times, you can turn this off.

"forceConsistentCasingInFileNames": true

Some Linux-based systems have case-sensitive file systems. That means that a file named MyFile.ts can’t be accessed by using import myFunction from './myFile.ts' . I’ve run into issues before where my app works perfectly on my non-case-sensitive development computer, but when I try to deploy it to a case-sensitive production environment, I discover I accidentally tried to import a file using the wrong casing. This option will warn you if you do that.

Implicit Type Checking

TypeScript’s type checker is designed to be easy to pick up and use. In fact, it will automatically infer, or guess, the types of many of the values in your program without you having to explicitly assign them a type. Take this for example. When we assign a string literal to a variable, TypeScript infers that the type of that variable is string .

let fruitName = "Banana"; // let name: string

Conceptually, we often refer to variables as being buckets that hold values. If that is the case, TypeScript lets us say what type of thing a bucket can hold and warns us if we ever try to put something in the wrong bucket. Here, we have a string bucket called “name”. TypeScript knows it’s a string because we’re immediately putting a string inside it.

If we were to try to reassign one of these variables to an incorrect type, TypeScript would throw a type error.

let fruitName = "Banana";

fruitName = 1193; // Type Error: Type 'number' is not assignable to type 'string'.

TypeScript is telling us that we can’t assign a number to a string variable. That’s like trying to put oil in a bucket of water — they just don’t mix.

This kind of type inference works for all of the primitive types, like number s and boolean s. TypeScript also infers the types of object literals.

let fruit = {
  name: "Banana",
  color: "yellow",
  sweetness: 80,
  isRipe: true,
};

// let person: {
//     name: string;
//     color: string;
//     sweetness: number;
//     isRipe: boolean;
// }

If we pull properties of the object into their own variables, those variables will still have the correct type.

let { name, sweetness } = fruit;

name; // const name: string
sweetness; // const sweetness: number

When you create an array literal, TypeScript will infer the type of that array as well.

let fruitNames = ["Apple", "Banana"]; // let fruitNames: string[]

TypeScript uses two square brackets ( [] ) after the type to denote arrays of that type.

Contextual Type Inference

TypeScript can also infer the types of variables based on the context where the variable was created. Take this array method for example.

let fruitNames = ["Apple", "Banana"];

// This function makes every other item in our list uppercase.
const alternatedFruitNames = fruitNames.map((name, index) => {
  // (parameter) name: string
  // (parameter) index: number
  if (index % 2 === 0) {
    return name.toUpperCase();
  }
  return name;
});

alternatedFruitNames; // const alternatedFruitNames: string[]

TypeScript already knows that when we run .map on an array, the first parameter’s type will be the type of an array item and the second parameter will be a number representing the current index of the loop. In this case, we didn’t have to tell TypeScript anything about our types; it already knew.

We could even transform our array from one type to another and TypeScript will recognize the transformation, like with this function that changes the array of strings into an array of numbers.

let fruitNames = ["Apple", "Banana"];

const nameLength = fruitNames.map((name) => {
  return name.length;
});

nameLength; // const nameLength: number[]

If we were to create a function and pass that to our array’s map function, TypeScript can’t infer the parameter types anymore. This is because the function is no longer directly associated with the .map function, so TypeScript can’t directly know how our transformName function will be used.

function alternateUppercase(name, index) {
  // (parameter) name: any
  // (parameter) index: any
  if (index % 2 === 0) {
    return name.toUpperCase();
  }
  return name;
}

fruitNames.map(alternatedFruitNames);

TypeScript’s type inference will always fall back on the special any type. This type represents any other type and behaves as if we were using JavaScript. This can be handy when it’s difficult to give something the appropriate type, but we lose the guarantees which TypeScript’s type checker gives us. We want to avoid implicitly any s, so in our config we turned on the noImplicitAny flag. With our configuration, we would get a type error:

function alternateUppercase(name, index) {
  // (parameter) name: any - Type Error: Parameter 'name' implicitly has an 'any' type.
  // (parameter) index: any - Type Error: Parameter 'index' implicitly has an 'any' type.
}

TypeScript is warning us that this function has lost type safety because of the implicit any type. Over the next few lessons, we’ll learn how to give our variables and functions explicit type annotations so we can always have type safety.

Adding Type Annotations

One of the best things about TypeScript, as compared to many other typed languages, is it supports implicit type checking. TypeScript automatically reads our code and determines the types of values for us so we don’t have to annotate all of our types.

However, there are times that TypeScript isn’t able to infer our types. This is often because we are defining a variable before assigning a value to it. If TypeScript isn’t able to infer our types properly, it will set the type to any , which means less type safety for us. In the example below, verbalFruitCount has an any type because TypeScript can’t tell what type it is supposed to be based on the way we use it.

function getFruitBasketVerbalCount(fruitList) {
  let verbalFruitCount; // let verbalFruitCount: any
  if (fruitList.length > 5) {
    verbalFruitCount = "A lot of fruit.";
  } else {
    verbalFruitCount = "Some fruit.";
  }
  return verbalFruitCount;
}

We need to add type annotations to tell TypeScript what type a variable should hold. Adding explicit types can help us avoid using values in a way that would cause errors at runtime. We do this by placing a colon after the variable declaration followed by the type.

let favoriteDessert: string;
favoriteDessert = 6; // Type Error: Type 'number' is not assignable to type 'string'.
favoriteDessert = "Cheesecake";

Notice that the types are lowercase. This is the case for string , number , boolean , and most other primitive types. JavaScript has built-in objects for all of its types which use capital case, including String , Number , Boolean , and Object . These are not the same as lowercase types which we use with TypeScript and we should almost never use the uppercase versions when adding type annotations.

We can assign types to variables for other types.

let numberOfGuests: number;
let menuPlanned: boolean;

Notice that we haven’t assigned any value to these variables. In JavaScript, these variables have the value of undefined . In TypeScript, adding a type definition tells TypeScript that our variable must be that type. If we try using it before that variable is assigned a value, TypeScript will assume this is undesired behavior and give us an error.

let floralArrangement: string;
console.log(floralArrangement); // Variable 'floralArrangement' is used before being assigned.

If we actually do intend to use the value whether it is undefined or not, we can use a non-null assertion to tell TypeScript that we don’t care if our variable is used before being assigned. It looks like an exclamation point before the colon:

let floralArrangement!: string;
console.log(floralArrangement); // undefined

Array types are defined using the square bracket ( [] ) syntax we saw earlier. There is another syntax for defining arrays with angle brackets, but it’s use is discouraged.

let ingredients: string[]; // use this syntax
let recipes: Array<string>; // don't use this syntax

Object types can also be defined by placing a colon after the property names.

let menu: {
  courses: number;
  veganOption: boolean;
  drinkChoices: string[];
};

Make sure you don’t mistake these types for using object destructuring to assign properties to new variable names. They may look similar, but in one case we’re creating a variable and in the other case we’re adding type annotations.

// These are renamed variables, not types.
let { courses: orderedFood, veganOption: hasVegan } = menu;

null and undefined can also be assigned as a type, but that isn’t very helpful, since the variable will only ever be able to hold null or undefined . In a future lesson, we’ll talk about how we can do more useful things with null and undefined using Union types.

We can also add annotations to a value that already has an any type. This restores TypeScript’s type checking to that value. One place this might be necessary is when performing network requests.

async function getFruitList() {
  const response = await fetch("https://example.com/fruitList");
  const fruitList = await response.json(); // const fruitList: any;
  const typeFruitList: string[] = fruitList; // const typeFruitList: string[];
  return typeFruitList;
}

Typing Function Declarations

Functions have a lot more moving parts than variables and objects, like parameters and return values. We can add type annotations to any of these things.

Typing parameters

Adding type annotations to function parameters is very similar to adding types to variables. Let’s return to our array map example.

let fruitNames = ["Apple", "Banana"];

function alternateUppercase(name, index) {
  // (parameter) name: any
  // (parameter) index: any
  if (index % 2 === 0) {
    return name.toUpperCase();
  }
  return name;
}

const alternatedFruitNames = fruitNames.map(alternateUppercase);

Since TypeScript can’t infer the types of our function’s parameters, we have to add the types manually. This looks very similar to adding types to variables - we use a colon ( : ) after the parameter name, followed by the type.

function alternateUppercase(name: string, index: number) {
  if (index % 2 === 0) {
    return name.toUpperCase();
  }
  return name;
}

Now TypeScript knows that our function takes a string and a number as parameters. What would happen if we accidentally used a function with the wrong types?

function doubleNumber(num: number) {
  return num * 2;
}

const alternatedFruitNames = fruitNames.map(doubleNumber);
// Types of parameters 'num' and 'value' are incompatible.
//     Type 'string' is not assignable to type 'number'.

TypeScript threw a type error. It knows that the values of the array are string s, so when we try to use a function that accepts numbers s instead of string s, it will warn us that something is not right.

Return Values

We can also add types for the value that a function returns.

function headsOrTails(): boolean {
  return Math.random() > 0.5;
}

This function most definitely returns a boolean value. Most of the time TypeScript will infer the return type by what is returned, but adding an explicit return type makes it so we can’t accidentally return a value of the wrong type.

Async Functions

By definition, an async function is a function which returns a JavaScript Promise. Just like arrays, there is a special syntax for defining the type of the value which is wrapped in a promise. We place the wrapped type in angle brackets and put Promise in front of it.

async function getFruitList(): Promise<string[]> {
  const response = await fetch("https://example.com/fruit");
  const fruitList: string[] = await response.json();
  return fruitList;
}

If we were to try annotating this function with just string[] , TypeScript would warn us that we need to use the Promise type wrapper.

Function Type Expressions

What do we do when we have a function that takes another function (often called a callback) as a parameter? For example, if I were to create a type definition for an array map function, I would have to pass a callback function as one of the parameters.

function mapNumberToNumber(list: number[], callback) {
  // (parameter) callback: any
  // Implementation goes here
}

Our callback parameter has an any type, which means we could call it as a function if we wanted to. However, we want to avoid any , since using it leads to less type safety.

We can create a function type annotation using a special syntax. It might look like an arrow function, but it’s defining a type.

function mapNumberToNumber(list: number[], callback: (item: number) => number) {
  // (parameter) callback: any
  // Implementation goes here
}

Then, when we call the function, TypeScript can check the callback that we pass in to make sure it matches the type signature we used.

const doubledNumbers = mapNumberToNumber([1, 2, 3], (num) => num * 2);

In this case, num is inferred to be a number because TypeScript is able to determine the type from the type annotation we added to the callback parameter.

Optional and Default Parameters

TypeScript expects that every parameter of a function will be passed to it when the function is called, even if its value is undefined . This can be a problem when we don’t want to require the user to pass in every single parameter every time.

function logOutput(message: string, yell: boolean) {
  if (yell) {
    console.log(message.toUpperCase());
    return;
  }
  console.log(message);
}

logOutput("Hey! Listen!");
// TypeError: Expected 2 arguments, but got 1.
//  An argument for 'yell' was not provided.

We can tell TypeScript that a parameter is optional by adding a ? right before the type annotation.

function logOutput(message: string, yell?: boolean) {
  if (yell) {
    console.log(message.toUpperCase());
    return;
  }
  console.log(message);
}

logOutput("Hey! Listen!"); // "Hey! Listen!"

We didn’t need to include the second yell parameter because we marked it as optional.

We can also mark parameters as optional by giving them a default value. TypeScript will infer the type of the parameter from the default value.

function logOutput(message: string, yell = true) {
  if (yell) {
    console.log(message.toUpperCase());
    return;
  }
  console.log(message.toUpperCase());
}

logOutput("Hey! Listen!"); // "HEY! LISTEN!"

Spread Parameters

When we aren’t sure how many parameters will be passed to a function, we can use the new spread syntax, which gives all of the parameters in a list. If all of the extra parameters are the same type, we can easily add an annotation to the spread parameters.

function logManyOutput(...messages: string[]) {
  messages.forEach((message) => {
    logOutput(message);
  });
}

(Practice) Type Annotations

Below is a CodeSandbox link which has some TypeScript code. Your job is to add type annotations to all of the variables and functions and eliminate all of the TypeScript warnings.

You’ll find some of the variables use the unknown type. This is a special TypeScript type which means we don’t know what type the value is. However, in this case, we do know what it is. You need to remove all of the unknown types and replace them with the correct type annotations.

Good luck!

Type Annotations CodeSandbox

(Solution) Type Annotations

Solution

Type Annotations CodeSandbox Solution

any and unknown types

There are plenty of times when writing TypeScript that we have no way of knowing what the type of something is. Other times, the type signature might be so complicated that it’s easier for us to decrease the type safety and pretend like we are writing JavaScript. TypeScript gives us two special types that represent values that could be any type: any and unknown .

any

Like we’ve seen before, the any type turns off the type checker and removes all guarantees that our types are correct. any can represent any type. It can also be assigned to variables of any type and you can try to access any property on it as if it were an object. You can even call it as if it were a function! This is really handy, but it can also be dangerous, since it’s hard to know what a variables type is at runtime.

One place any is used is with network calls, since you can’t know what type the network call will return without adding an annotation.

async function getFruitList() {
  const response = await fetch("https://example.com/fruit");
  const fruitList = await response.json(); // const fruitList: any;
}

Since we can assign any values to variables with any type, we can add a type annotation to the variable we’re putting our results into and TypeScript will start type checking the value.

async function getFruitList() {
  const response = await fetch("https://example.com/fruit");
  let fruitList: string[];
  fruitList = await response.json(); // const fruitList: string[];
}

We have to be really careful any time we use any . If this network request returns anything other than a string[] , we might run into runtime type errors. For example, if it were an object instead of an array of strings, trying to use array methods like .map() would fail with the fruitList.map is not a function type error.

unknown

unknown is the type safe version of any . It still can represent any type, but since we don’t know what type it is specifically, TypeScript limits what we are able to do with values of this type. You can assign values of any type to a variable with the unknown type, but you can’t assign unknown values to variables of other types. We also can’t access properties on unknown values. Really, you can’t do anything with unknown except pass it around.

const unknownString: unknown = "What am I?";
let stringValue: string = unknownString; // Type Error: Type 'unknown' is not assignable to type 'string'.

const unknownNumber: unknown = 27;
let theAnswer = 15 + unknownNumber; // Type Error: Operator '+' cannot be applied to types 'number' and 'unknown'.

How is this even helpful? It might give us some type guarantees, but it also means we can’t do anything with our values. Fortunately, we can convince TypeScript that an unknown or any value actually has a more specific type by using a process called type narrowing. This involves doing runtime checks which either prove that a value is a specific type or prove that it is not a specific type.

In this case, we’ll use JavaScript’s typeof operator to prove that our value is a number .

const unknownNumber: unknown = 27;

let theAnswer: number = 0;
if (typeof unknownNumber === "number") {
  theAnswer = 15 + unknownNumber;
}

TypeScript understands that within that if statement, our unknownNumber variable is actually a number .

We’ll learn about more methods of type narrowing in future lessons.

any or unknown ?

When working with any kind of dynamic data, such as user input or API responses, you’ll have some degree of uncertainty with the types of the data you use. That means at some point you’ll have to use any or unknown , but how do you know when to use one over the other?

any gives you the greatest flexibility, but absolutely no type safety, so it’s best to avoid any unless it is absolutely necessary. Using unknown doesn’t give you much flexibility, but it does maintain TypeScript’s type safety guarantee, and encourages you as the developer to add the necessary checks to narrow your value’s type to something more useful.

As a general rule of thumb, prefer unknown over any and use type narrowing to determine a more accurate type.

Interfaces

We’ve already learned that we can add type definitions for objects directly to the variable when we’re assigning it, like so:

const car: { wheels: number; color: string; electric: boolean } = {
  wheels: 4,
  color: "white",
  electric: true,
};

This defines the shape of the object, and tells us that our object has a number of wheels and a color. This works well for one-off objects, but what if we have many objects that all have the same shape? We’ll end up having lots of duplication.

Fortunately, we can construct a special type definition for our object using Interfaces. Interfaces let us create a named definition of the shape of an object that we can reuse. Once we’ve defined our interface type, we can use it as a type annotation.

interface Vehicle {
  wheels: number;
  color: string;
  electric: boolean;
}
const car: Vehicle = { wheels: 4, color: "white", electric: true };
const motorcycle: Vehicle = { wheels: 2, color: "red", electric: false };
const tractorTrailer: Vehicle = { wheels: 18, color: "blue", electric: false };

This also helps us tell the difference between two different objects that have a similar shape.

interface Fruit {
  name: string;
  color: string;
  calories: number;
  nutrients: Nutrient[];
}
interface Vegetable {
  name: string;
  color: string;
  calories: number;
  nutrients: Nutrient[];
}

let apple: Fruit;
let squash: Vegetable;

Just based on the types, we can see that an apple is a Fruit and a squash is a Vegetable. Also, notice that we can use interfaces as the base type for arrays as is the case with our Nutrient[] type.

We should recognize though, that because Fruit and Vegetable have the same shape, they are essentially equivalent. We could assign a Fruit variable to a Vegetable variable, and TypeScript wouldn’t complain. This is because Interfaces don’t create new types; they just assign names to a particular shape of types. This also means that a literal object with the same shape as a Fruit or Vegetable is also compatible.

let fruitBasket: Fruit[] = [];

const tomato = { name: "Tomato", color: "red", calories: 10, nutrients: [] };
fruitBasket.push(tomato); // It works.

We’ll talk about how to get around this behavior using Discriminating Unions in a future section.

Extending Interfaces

We can see that our Fruit and Vegetable Interfaces are very similar - we might as well just have one Interface and use it for both of them. But what if there were properties that were unique to either Fruit or Vegetable? To handle this, we could extend our interface. This copies the property definitions of one interface to another, which lets you reuse type definitions even more. Lets look at how we could improve our previous example.

interface EdibleThing {
  name: string;
  color: string;
}

interface Fruit extends EdibleThing {
  sweetness: number;
}

const apple: Fruit = { name: "apple", color: "red", sweetness: 80 };

Declaration Merging

Interfaces can be declared multiple times with different properties each time. When TypeScript compiles your code, it will combine the two interfaces together, allowing you to use properties from both of them.

interface Fruit {
  name: string;
}
interface Fruit {
  color: string;
  sweetness: number;
}

const apple: Fruit = { name: "apple", color: "red", sweetness: 80 };

Object Literal Assignment

When TypeScript is checking to see if a value can be assigned to an Interface, it is going to look at each of the properties to see if the types of those properties match up. When assigning one variable to another, it only checks the properties which are defined on the Interface, not all of the properties on the variable. That makes it possible to put more properties on a variable than are defined by the interface. We won’t be able to access those properties without getting a type error, but they are still there.

interface Fruit {
  name: string;
  color: string;
  sweetness: number;
}
let fruitBasket: Fruit[] = [];

const tomato: {
  name: string;
  color: string;
  sweetness: number;
  hasSeeds: boolean;
} = { name: "Tomato", color: "red", sweetness: 10, hasSeeds: true };
fruitBasket.push(tomato);

fruitBasket[0].hasSeeds; // Property 'hasSeeds' does not exist on type 'Fruit'.
console.log(fruitBasket[0]); // { name: "Tomato", color: "red", sweetness: 10, hasSeeds: true }

TypeScript has a special rule for object literals, however. If you try to assign an object literal to a specific type, TypeScript will warn you about any extra properties.

fruitBasket.push({
  name: "Banana",
  color: "yellow",
  sweetness: 70,
  hasSeeds: false,
});
// Argument of type '{ name: string; color: string; sweetness: number; hasSeeds: boolean; }' is not assignable to parameter of type 'Fruit'.
//    Object literal may only specify known properties, and 'hasSeeds' does not exist in type 'Fruit'.

TypeScript is telling us that we can’t push our object literal onto an array of Fruit s because we have an extra property which isn’t defined on the Fruit interface. Again, this is a special behavior for object literals that doesn’t apply when assigning one variable to another.

Optional Properties

Sometimes properties on an interface are entirely optional. Or we will assign them a value later, but don’t have that value right now. We can specify properties of interfaces as optional using a question mark.

interface Fruit extends EdibleThing {
  calories?: number;
}

In this case, we might not have the calorie count for our fruit before initially defining it, so we mark it as optional. Since we’re overriding the calories definition on our EdibleThing interface, calories become optional for any Fruit, but not any other EdibleThing . If we were to try to access calories on an instance of a Fruit , it would return undefined . If we had “strictNullChecks” enabled in our tsconfig.json, we would have to use type narrowing to make sure that our fruit actually had a value in that property.

interface Fruit extends EdibleThing {
  calories?: number;
}
let totalCalories = 0;
let apple: Fruit = { name: "Apple", color: "red" };

totalCalories += apple.calories; // Object is possibly 'undefined'.

if (apple.calories) {
  // "apple.calories" is now recognized as a number
  totalCalories += apple.calories; // This works
}

Incidentally, optional properties can be applied to regular object definitions too.

let pear: { name: string; calories?: number };

Indexable Types

If we know the property exists on the interface, we can just access it directly using dot ( . ) notation, as in Math.random or Array.isArray . But what if we are using a value in a variable to access a property dynamically? In JavaScript, we use square brackets to do that.

const propertyName = "random";
Math[propertyName](); // 0.2524323113

That’s where Indexable Types come in. Using an index signature , we can provide types for anything that is accessed dynamically using a variable as the property name. When we define our index signature, we need to provide the type of the index itself (only string and number is allowed), an identifier for the index (we use “key” in this next example), and the type of the property’s value. We can mix index signatures with regular property signatures, so long as the key and value types match.

interface Fruit {
  [key: string]: string;
  name: string;
}

let apple: Fruit = {
  name: "Apple",
  ripeness: "overripe",
};

If we were to use an index of the wrong type, or if we were to use the wrong value, TypeScript would throw a type error.

let apple: Fruit = {
  name: "Apple",
  isRipe: true, // Type 'boolean' is not assignable to type 'string'.
  //     The expected type comes from the index signature.
};

Notice that the type error tells us both that we can’t use boolean s in place of string s and that the restriction is because of the index signature.

We can use number s for our index signatures to represent array-like objects.

interface FavoriteFruitList {
  [fruitOrder: number]: string;
}

const favoriteFruit: FavoriteFruitList = [];
favoriteFriends[1] = "Apple";

const thirdPlace = 3;
favoriteFriends[thirdPlace] = "Strawberry";

Notice that we can use any identifier we want for our index. In this case, we used “fruitOrder”. This doesn’t affect how the code works at all. This is really helpful for documenting what the index type actually represents.

(Practice) Interfaces

Below is a CodeSandbox link which has some TypeScript code. Your job is to create and extends some interfaces so all of the TypeScript warnings go away.

Good luck!

Interfaces CodeSandbox

(Solution) Interfaces

Solution

Interfaces CodeSandbox Solution

Enum and Tuple Types

TypeScript gives us two types which expand on object and arrays : Enums and Tuples. The purpose of both of these types is to add even more structure to our types. Lets take a look at some situations when these might be helpful.

Enums

One common programming tip is to avoid using magic values in our code. These are values which change how our program behaves, but the values themselves aren’t named. Take this function for example:

function seasonsGreetings(season: string) {
  if (season === "winter") return "⛄️";
  if (season === "spring") return "🐰";
  if (season === "summer") return "🏖";
  if (season === "autumn") return "🍂";
}

In this case, the string values winter , spring , summer , and autumn are magic strings. Having them in just one function is fine, but it we were to reuse them over and over in our codebase, there’s a chance we might have a typo or miss one of the options. Also, it might not be clear what the magic value represents. That’s why it’s common to put our magic values into named constants.

const SEASONS = {
  winter: "winter",
  spring: "spring",
  summer: "summer",
  autumn: "autumn",
};

function seasonsGreetings(season: string) {
  if (season === SEASONS.winter) return "⛄️";
  // ...
}

This puts our magic strings in one place, gives them a name, and makes us less likely to misspell them, since TypeScript will warn us if we use the wrong property name for SEASONS . This doesn’t solve all of the problems, though. We can still pass any string into our seasonsGreetings function, and our object definition is a little verbose. We can use an Enum to create a type safe definition of named constants which we can reference elsewhere in our code.

Here is our same function, but implemented with an Enum instead of an object.

enum Seasons {
  winter,
  spring,
  summer,
  autumn,
}

function seasonsGreetings(season: Seasons) {
  if ((season = Seasons.winter)) return "⛄️";
  // ...
}

const greeting = seasonsGreetings(Seasons.winter);

Notice that we are able to use Seasons as both a type and a value. We tell TypeScript that the season parameter of our seasonsGreetings function is a Seasons type, which means it has to be one of the constant properties we defined in our Enum. Then, when we call our function, instead of passing a string , we pass one of the properties of Seasons into the function.

Our Enum acts like an object, where the string s we include are the property names and their values are incrementing number s, starting at 0. Notice that when we assign the Enum as a type for a variable, we can use any of the properties of the Enum as values for that variable.

Most types are removed when TypeScript compiles code to JavaScript. Enums, on the other hand, are translated into JavaScript snippets which represent their shape and behavior. That makes Enums both a type and a value. If I were to run the Seasons enum through the TypeScript compiler, this is what it would output:

var Seasons;
(function (Seasons) {
  Seasons[(Seasons["winter"] = 0)] = "winter";
  Seasons[(Seasons["spring"] = 1)] = "spring";
  Seasons[(Seasons["summer"] = 2)] = "summer";
  Seasons[(Seasons["autumn"] = 3)] = "autumn";
})(Seasons || (Seasons = {}));

console.log(Seasons);
// {
//   '0': 'winter',
//   '1': 'spring',
//   '2': 'summer',
//   '3': 'autumn',
//   winter: 0,
//   spring: 1,
//   summer: 2,
//   autumn: 3
// }

There are a few things to learn from this. First, TypeScript implements enums as an object very similar to the object we used before. However, it uses number s to represent the constants instead of string s. Also, we can see that Enums allow us to both access the number s using the property names, but also access the property names with the appropriate number index.

If we wanted to start our enum with a different number , we just put it in front of the first item and the rest will auto-increment.

enum Seasons {
  winter = 12,
  spring,
  summer,
  autumn,
}
console.log(SEASONS.summer); // 14

Or we could assign each Enum property its own number .

enum Seasons {
  winter = 15,
  spring = 200,
  summer = 12,
  autumn = 59,
}

We can even assign Enum properties string values. Remember that if you do this, you have to assign string s to all of the properties.

enum Seasons {
  winter = "snowy",
  spring = "rainy",
  summer = "sunny",
  autumn = "windy",
}
const mySeason = Seasons.winter; // const mySeason: Seasons
console.log(mySeason); // "snowy"

One thing to remember is that Enums are their own types with a unique behavior. You can actually assign number s to Enum variables so long as the Enum doesn’t use string values. However, you cannot assign a string property to an Enum variable, even if the Enum uses string values. We have to use the properties of the Enum instead.

enum Colors {
    red,
    green,
    blue
}
let myEnum = Colors.red;
let myNumber: number = myEnum; // This works
myEnum = 2 // This also works

enum Seasons {
    spring = "spring"
    summer = "summer"
    autumn = "fall"
    winter = "winter"
}
let myStringEnum = Seasons.spring
myStringEnum = "winter" // Type 'string' is not assignable to type 'Seasons'.
myStringEnum = Seasons.winter // This works

Enums are often used to represent the names of different finite states. For example, we could use an Enum to model the different states of a Promise.

enum PromiseStates {
  pending,
  fulfilled,
  rejected,
}
const FakePromise = {
  state: PromiseStates.pending,
  resolve: function () {
    this.state = PromiseStates.fulfilled;
  },
  reject: function () {
    this.state = PromiseStates.rejected;
  },
};

Tuple

Most of the time when we use arrays, we intend for it to be variable length, which means we can add and remove items from the array. We also expect all of the elements to be the same type. The type string[] means an array of any size that can only be strings. What if we had an array where different items were different types?

For example, the useState hook in React returns an array of two things: The state value, and a function to update the state. Here’s a very simplified, naïve implementation of useState .

let simpleState: string;
function simpleUseState(initialState: string) {
  if (!simpleState) {
    simpleState = initialState;
  }
  function updateState(newState: string) {
    simpleState = initialState;
  }
  return [simpleState, updateState];
}

const [username, setUsername] = simpleUseState("alexanderson");
// const username: string | ((newState: string) => void)
// const setUsername: string | ((newState: string) => void)

We can see that our naïve solution didn’t hold up well. Instead of giving us an array where the first item is a string and the second item is a function, simpleUseState returned an array where each item is either a string or a function. These are called Union types, and they are annotated with the vertical bar ( | ) between the different types. We’ll learn more about them later on in the course.

For right now, all that we need to know is that TypeScript won’t let us use username or setUsername as a string or function without us checking to see which it is first. We can do this with type narrowing.

setUsername("Alex");
// Type Error: Not all constituents of type 'string | ((newState: string) => void)' are callable.
//     Type 'string' has no call signatures.

if (typeof setUsername === "function") {
  setUsername("Alex");
}

This would be much nicer if we could tell TypeScript that the first item in our array is a string and the second is a function. We can do that using Tuples.

Tuples are fixed-length arrays. We can tell TypeScript how many items are in the array, and what the type of each item is.

We write Tuples by wrapping our list of types in square brackets.

function simpleUseState(
  initialState: string
): [string, (newState: string) => void] {
  // The rest of the implementation goes here.
}

TypeScript will never infer an array of items to be a tuple, even if the items are of different types. Like we saw with our naïve example, TypeScript guessed we wanted an array of mixed types, not specific types for each index in the array. That means that when you create tuples, you always have to add a type annotation.

Destructuring our Tuple will yield the appropriate types on our values without us having to annotate them.

const [username, setUsername] = simpleUseState("alexanderson");
// const username: string
// const setUsername: (newState:string) => void
setUsername("Alex"); // No error

Void and Never Types

void

void represents the absence of any type. It’s often used to indicate that a function doesn’t return anything.

function sendRequestAndForget() {
  fetch("https://example.com/addUser", { method: "POST" });
}

const output = sendRequestAndForget(); // const output: void;

It’s common to use this type when annotating callback functions that don’t return.

function performRequest(requestCallback: () => void) {
  // implementation goes here
}

It’s also a good idea to annotate the return type of your functions with void when you know that the function won’t return anything. This will give you a warning if you ever try to return something on accident.

Using it outside the context of function return values isn’t very helpful, since you can’t assign anything to void variables. Stick to using it with functions that return nothing.

never

We’ve seen what happens when we have a function that doesn’t return anything. What about functions that can never return anything.

function exception() {
  throw new Error("Something terrible has happened");
}

const output = exception(); // const output: void;

function loopForever() {
  while (true) {}
}

TypeScript has inferred the type of these functions as void . However, that’s not totally accurate. Our first function will never return because of the throw statement, and the second has an infinite loop. TypeScript gives us a type which lets us be more specific about this function’s return type.

function exception(): never {
  throw new Error("Something terrible has happened");
}

const output = exception();
// const output: never;

function loopForever(): never {
  while (true) {}
}

const loopOutput = loopForever();
// const loopOutput: never;

Instead of representing the absence of a type like void , never represents a value that can never occur. If we every try to access or modify a never variable, TypeScript will give us a warning.

Usually we have to explicitly annotate our function with the never type. There is one case where TypeScript will correctly infer the return type as never : returning the results of a never function.

function implicitNever() {
  return exception();
}
const output = implicitNever(); // const output: never;

There are other circumstances where we might see never show up. For example, we’ll see never again when looking at Intersection types, and later on when working with Conditional types. Just remember that never represents a type that will never exist at runtime.

type aliases

Interfaces allow us to give names to the shape of objects. Objects aren’t the only types we encounter in our program, though. For example, we might use the same Tuple type in multiple placed, which means redefining it several times over. In this first example, we use a tuple of three number s to represent a point in 3D space.

let position: [number, number, number] = [27, 31, 5];

function calculateDistance3D(
  point1: [number, number, number],
  point2: [number, number, number]
): number {
  // TODO: Use distance formula
}

That’s a lot of duplication, and a Tuple of three numbers could represent many different things.

type aliases allow us to give names to any other type or combination of types. It looks very similar to setting a variable, except instead of let or const we use type , and instead of a value we use a type annotation. Once we’ve defined our alias, we can use it on variable definitions the same way we use interfaces.

Let’s see what our 3D coordinates look like as a type alias.

type Coordinates3D = [number, number, number];

let position: Coordinates3D = [27, 31, 5];

function calculateDistance3D(
  point1: Coordinates3D,
  point2: Coordinates3D
): number {
  // TODO: Use distance formula
}

Now our function declaration is much cleaner and we can see more clearly what the type represents.

Any type or type combination can be used with a type alias.

type FruitList = string[];

const fruit: FruitList = ["Apple", "Orange"];

Writing type aliases doesn’t create a new type; any value that is compatible with the alias’ type will be compatible with the alias. For example:

type FruitList = string[];
interface IndexedFruitList {
  [index: number]: string;
}

const fruit: FruitList = ["Apple", "Orange"];
const otherFruitList: string[] = fruit; // This works
const indexedFruitList: IndexedFruitList = fruit; // This also works

We can do more interesting things with type aliases too, like self-referential types. Here’s a type which represents a tree of strings. Optionally, we can put a left or right value, which is another branch on the tree with its own value and its own left and right branch.

type StringTree = {
  value: string;
  left?: StringTree;
  right?: StringTree;
};

let myStringTree: StringTree = getStringTree();
myStringTree.value; // string
myStringTree?.left?.right?.left?.value; // string | undefined

Above, you might have noticed the interesting string | undefined type annotation. Remember that adding ?: to our type annotations marks those types as optional, which means they might be undefined . We use optional chaining to access the value, but using optional chaining means the value could be a string , but it might be undefined . As we’ve seen before, a type that could be two or more different types is called a Union type, and it’s annotated with the vertical bar ( | ), like we just saw. We’ll cover this more in the next section.

Interface or type ?

Interfaces and type aliases are very similar. You might even be wondering why TypeScript has two constructs which perform basically the same function. They have subtle differences which can make the difference when deciding whether to use one over the other.

Interfaces support extension using the extends keyword, which allows an Interface to adopt all of the properties of another Interface. Because of this, Interfaces are most useful when you have hierarchies of type annotations, with one extending from another.

type aliases, on the other hand, can represent any type, including functions and Interfaces!

interface Fruit {
  name: string;
  color: string;
  sweetness: number;
}

type FruitType = Fruit;

type EatFruitCallback = (fruit: Fruit) => void;

When you find yourself writing the same type signature repeatedly, or if you want to give a name to a particular type signature, you want to use a type alias. type aliases will become more important as we explore more complicated type compositions, such as Union and Intersection types, Generics, and utility types. We’ll be diving deeper into all of these in future sections.