The TypeScript Handbook

I want to read through "The TypeScript Handbook" so that I can improve my knowledge of TypeScript and define cleaner types.

Date Created:
Last Edited:
1 45

References



The most common types of errors that [JavaScript] programmers write can be described as type errors: a certain kind of value was used where a different kind of value was expected. This could be due to simple typos, a failure to understand the API surface of a library, incorrect assumptions about runtime behavior, or other errors. The goal of TypeScript is to be a static typechecker for JavaScript programs - in other words, a tool that runs before your code runs (static) and ensures that the types of the program are correct (typechecked).


TypeScript for JavaScript Programmers


TypeScript offers all of JavaScript's features, and an additional layer on top of all of these: TypeScript's type system. The main benefit of TypeScript is that it can highlight unexpected behavior in your code, lowering the chance of bugs.

By understanding how JavaScript works, TypeScript can build a type-system that accepts JavaScript code but has types. This offers a type-system without needing to add extra characters to make types explicit in your code.

TypeScript supports an extension of the JavaScript language, which offers places for you to tell TypeScript what the types should be.

  1. You can create an object with an inferred type which includes name: string and id: number
const user = {
name: "Hayes",
id: 0,
};
  1. You can explicitly describe this object's shape using an interface declaration
interface User {
name: string;
id: number;
}
  1. You can declare that a JavaScript object conforms to the shape of your new interface by using the syntax like : TypeName after a variable declaration:
const user: User = {
name: "Hayes",
id: 0,
};

Since JavaScript supports classes and object-oriented programming, so does TypeScript. You can use an interface declaration with classes:

interface User {
name: string;
id: number;
}
 
class UserAccount {
name: string;
id: number;
 
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
 
const user: User = new UserAccount("Murphy", 1);

You can use interfaces to annotate parameters and return values to functions:

function deleteUser(user: User) {
// ...
}
 
function getAdminUser(): User {
//...
}

There is already a small set of primitive types available in JavaScript: boolean, bigint, null, number, string, symbol, and undefined, which you can use in an interface. TypeScript extends this list with a few more, such as any (allow anything), unknown (ensure someone using this type declares what the type is), never (it's not possible that this type could happen), and void (a function which returns undefined or has no return value).

There are two interfaces for building types: Interfaces and Types. You should prefer interface. Use type when you need specific features.

You can create types in TypeScript by combining simpler ones: Unions and Generics.

Unions

With a union, you an declare a type that could be one of many types.

type MyBool = true | false;
type WindowStates = "open" | "closed" | "minimized";
type LockStates = "locked" | "unlocked";
type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;
Generics

Generics provide variables to types. A common example is an array. An array with generics can describe the values that the array contains.

type StringArray = Array<string>;
type NumberArray = Array<number>;
type ObjectWithNameArray = Array<{ name: string }>;

You can declare your own types that use generics:

// @errors: 2345
interface Backpack<Type> {
  add: (obj: Type) => void;
  get: () => Type;
}

// This line is a shortcut to tell TypeScript there is a
// constant called `backpack`, and to not worry about where it came from.
declare const backpack: Backpack<string>;

// object is a string, because we declared it above as the variable part of Backpack.
const object = backpack.get();

// Since the backpack variable is a string, you can't pass a number to the add function.
backpack.add(23);

Structural Type System

One of TypeScript's core principles is that type checking focuses on the shape that values have. This is sometimes called duck typing or structural typing. In a structural type system, if two objects have the same shape, they are considered to be of the same type.

interface Point {
  x: number;
  y: number;
}

function logPoint(p: Point) {
  console.log(`${p.x}, ${p.y}`);
}

// logs "12, 26"
const point = { x: 12, y: 26 }; // Type defined implictly, not explicitly as type Point
logPoint(point);


The Basics


JavaScript only truly provides dynamic typing - running the code to see what happens. The alternative is to use a static type system to make predictions about what the code is expected to do before it runs. Static type systems describe the shapes and behaviors of what our values will be when we run our programs. A type checker like TypeScript uses that information and tells us when things might be going off the rails.

$ npm install -g typescript # Install the TypeScript type-checker
$ tsc hello.ts # Check TypeScript file, compile and transform file into plain JS file

There are some times when type checks get in the way. The noEmitOnError compiler option allows you to transpile typescript files that have type errors.

Type annotations aren't part of JavaScript (or ECMAScript), so there really aren't any browsers or other runtimes that can just run TypeScript unmodified. That's why TypeScript needs a compiler/transpiler in the first place - it needs some way to strip out or transform any TypeScript-specific code so that you can run it. TypeScript has the ability to rewrite code from newer versions of ECMAScript to older ones such as ECMAScript 3 or ECMAScript 5. The process of moving from a newer or higher version of ECMAScript down to an older or lower one is sometimes called downleveling. You can set which version of ECMAScript to target by setting the target option.

When possible , a new codebase should always turn these strictness checks on. TypeScript has several type-checking strictness flags that can be turned on or off. The strict flag in the CLI, or "strict": true in tsconfig.json toggles them all simultaneously, but you can opt out of them individually. The two biggest ones are noImplicitAny and strictNullChecks.

noImplicitAny

In some places, TypeScript doesn't try to infer types for us and instead falls back to the most lenient type: any. Using any often defeats the purpose of using TypeScript in the first place. The more typed your program is, the more validation and tooling you'll get, meaning you'll run into fewer bugs as you code. Turn on the noImplicitAny flag will issue an error on any variables whose type is implicitly inferred as any.

strictNullChecks

By default, values like null and undefined are assignable to any other type. This can make writing some code easier, but forgetting to handle null and undefined is the cause of countless bugs. The strictNullChecks flag makes handling null and undefined more explicit, and spares us from worrying about whether we forgot to handle null and undefined.


Everyday Types


Three most common primitives (note: you should use the lowercase version of these types; capitalized versions of these types are available but refer to some special built-in types that will very rarely appear in your code.):

  1. string
  2. number
  3. boolean

Arrays: Array<number> or number[]

any can be used whenever you don't want a particular value to cause type-checking errors.

When you declare a function, you can add type annotations after each parameter to declare what type of parameters the function accepts. Parameter type annotations go after the parameter name. You can also add return type annotations. Return type annotations appear after the parameter list. Much like variable type annotations, you usually don't need a return type annotation because TypeScript will infer the function's type based on its return statements.

contextual typing is when the context that the function occurred within informs what type it should have. You can use , or ; to separate properties in an object type definition, and the last separator is optional either way. The type part of each property is optional. if you don't specify a type, it will be assumed to be any. Object types can also specify that some or all of their properties are optional. To do this, add a ? after the property name.

TypeScript will only allow an operation if it is valid for every member of the union. For example, if you have the union string | number, you can't use methods that are only available on string.

Working with Union Types

The solution is to narrow the union with code, the same as you would in JavaScript without type annotations. Narrowing occurs when TypeScript can deduce a more specific type for a value based on the structure of the code.

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}

A type alias is exactly that - a name for any type. The syntax for type alias is:

type Point = {
  x: number;
  y: number;
};

An interface declaration is another way to name an object type:

interface Point {
  x: number;
  y: number;
}

TypeScript is only concerned with the structure of the value we passed to printCoord - it only cares that it has the expected properties. Being concerned only with the structure and capabilities is why we call TypeScript a structurally typed type system.

Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

Differences between Type and Interface

Sometimes, you will have information about the type of a value that TypeScript can't know about. In this situation, you can use a type assertion to specify a more specific type:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
// You can also use angular bracket assertion
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

TypeScript only allows type assertions which convert to a more specific or less specific version of a type. This rule prevents impossible coercions like:

const x = "hello" as number;

Sometimes this rule can be too conservative. If this happens, you can use two assertions, first to any (or unkown), then to the desired type:

const a = expr as any as T;

You can use as const to convert an entire object to be type literals:

const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);

TypeScript has a special syntax for removing null and undefined from a type without doing any explicit checking. Writing ! after any expression is effectively a type assertion that the value isn't null or undefined:

function liveDangerously(x?: number | null) {
  // No error
  console.log(x!.toFixed());
}

Enums are a feature added to JavaScript by TypeScript which allows for describing a value which could be one of a set of possible named constants. Unlike most TypeScript features, this is not a type-level addition to JavaScript but something added to the language and runtime.


Narrowing


Much like how TypeScript analyzes runtime values using static types, it overlays type analysis on JavaScript's runtime control flow constructs like if/else, conditional ternaries, loops, truthiness checks, etc., which can all affect those types. A type guard is something like typeof padding === "number". TypeScript follows possible paths of execution that our programs can take to analyze the mot specific possible type of a value at a given position. The process of refining types to more specific types than declared is called narrowing.

JavaScript has an operator for determining if an object or its prototype chain has a property with a name: the in operator. TypeScript takes this into account as a way to narrow down potential types.

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }

  return animal.fly();
}

instanceof can be used as a type guard.

Assignability always checks against the declared type. The analysis of code based on reachability is called control flow analysis, and TypeScript uses this flow analysis to narrow types as it encounters type guards and assignments. When a variable is analyzed, control flow can split off and re-merge over and over again, and that variable can be observed to have a different type at each point.

To define a user-defined type guard, we simply need to define a function whose return type is a type predicate.

type Fish = { swim: () => void };
type Bird = { fly: () => void };
declare function getSmallPet(): Fish | Bird;
// ---cut---
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

Discriminated Union Example:

interface Circle {
  kind: "circle";
  radius: number;
}
interface Square {
  kind: "square";
  sideLength: number;
}
type Shape = Circle | Square; // Shape is a discriminated Union

When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use never type to represent a state which shouldn't exist.


More on Functions


The simplest way to describe a function is with a function type expression. These types are syntactically similar to arrow functions.

function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}
function printToConsole(s: string) {
  console.log(s);
}
greeter(printToConsole);

The syntax (a: string) => void means a function with one parameter, named a, of type string, that doesn't have a return value. Just like with function declarations, if a parameter type isn't specified, it's implicitly any.

In JavaScript, functions can have properties in addition to being callable. However, the function type expression syntax doesn't allow for declaring properties. If we want to describe something callable with properties, we can write a call signature in an object type:

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}

function myFunc(someArg: number) {
  return someArg > 3;
}
myFunc.description = "default description";

doSomething(myFunc);

It is common to write a function where the types of the input relate to the type of the output, or where the types of two inputs are related in some way. In TypeScript, generics are used when we want to describe a correspondence between two values. We do this by declaring a type parameter in the function signature:

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

We can use a constraint to limit the kinds of types that a type parameter can accept.

function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}

Because we constrained Type to { length number }, we were allowed to access the .length property of the a and b parameters.

You can make a parameter optional in TypeScript with ?.

In TypeScript, you can specify a function that can be called in different ways by writing overload signatures. To do this, write some number fo function signatures (usually two or more) followed by the body of the function.

// @errors: 2575
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);

This example shows two overloads: one accepting one argument, and another accepting three arguments. The first two signatures are called the overload signatures. Then, we wrote a function implementation with a compatible signature. Functions have an implementation signature, but this signature can't be called directly. The implementation signature must also be compatible with the overload signatures.

Function Overload Idiosyncracies

void represents the return value of functions which don't return a value. void and undefined are not the same thing in TypeScript. The special type object refers to any value that isn't a primitive (string, number, boolean, symbol, null, or undefined). This is different from the empty object type { } and also different from the global type Object (which you will likely never use). The unkown type represents any value. This is similar to the any type, but it is safer because it's not legal to do anything with an unkown value.

Some functions never return a value:

function fail(msg: string): never {
throw new Error(msg);
}

The never type represents values which are never observed. The global type Function describes properties like bind, call, apply, and others present on all function values in JavaScript.

The global type Function describes properties like bind, call, apply, and others present on all function values in JavaScript. It also has the special property that values of type Function can always be called, these calls return any:

function doSomething(f: Function) {
return f(1,2,3);
}

This is an untyped function call and is generally best avoided because of the unsafe any return type. If you need to accept an arbitrary function but don't intend to call it, the type () => void is generally safer.

We can define functions that take an unbounded number of arguments using rest parameters. A rest parameters appears after all other parameters, and uses the ... syntax.

function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);


Object Types


In JavaScript, the fundamental way that we group and pass around data is through objects. In TypeScript, we represent those through object types.

You mark a property optional by adding a question mark ? to the end of their names. Properties can also be marked as readonly for TypeScript. While it won't change any behavior at runtime, a property marked as readonly can't be written to during type-checking.

Readonly Properties

Sometimes you don't know all the names of a type's properties ahead of time, but you do know the shape of the values. In those cases, you can use an index signature to describe the types of possible values, for example:

interface StringArray {
[index: number]: string
}

This index signature states that when a StringArray is indexed with a number, it will return a string. Properties of different types are acceptable if the index signature is a union of the property types.

interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a numbetr
name: string; // ok, name is a string
}

The extends keyword on an interface allows us to effectively copy members form other named types, and add whatever new members we want. This can be useful for cutting down the amount of type declaration boilerplate we have to write, and for signaling the intent that several different declarations of the same property might be related.

interface BasicAddress {
name?: strng;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}

interfaces can also extend multiple types:

interface Colorful { color: string }
interface Circle { radius: number }
interface ColorfulCircle extends Colorful, Circle {}

interfaces allowed us to build up new types from other types by extending them. TypeScript provides another construct called intersection types that is mainly used to combine existing object types. An intersection type is defined using the & operator.

interface Colorful { color: string }
interface Circle { radius: number }
type ColorfulCircle = Colorful & Circle;

The principle difference between extends and intersections is how conflicts are handled, and that difference is typically one of the main reasons why you'd pick one over the other between an interface and a type alias of an intersection type.

You can make a generic object type which declares a type parameter.

interface Box<Type> {
contents: Type;
}

Modern JavaScript also provides other data structures which are generic, like Array<T>, Map<K,V>, Set<T>, and Promise<T>. All this means is because of how Map, Set, and Promise behave, they can work with any sets of types.

The ReadOnlyArray is a special type that describes arrays that shouldn't be changed.

A tuple type is another sort of Array type that knows exactly how many elements it contains, and exactly which types it contains at specific positions.

type StringNumberPair = [string, number];

Tuples can also have rest elements.

type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];

There are also readonly tuple types.


Type Manipulation


TypeScript's type system is very powerful because it allows expressing types in terms of other types. The simplest form of this idea is generics. Additionally, we have a wide variety of type operators to use. It's also possible to express types in terms of what values that we already have.

Generics

One of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one.

function identity<Type>(arg: Type): Type {
return arg;
}
let output = identity<string>("myString"); // Calling Generic
let output = identity("myString"); // type argument inference

function loggingIdentity<Type>(arg: Type[]):Type[] {
console.log(arg.length);
return arg;
}
interface LengthWise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property
return arg;
}

Keyof Type Operators

The keyof operator takes an object type and produces a string or numeric literal union of its keys. The following type P is the same as type P = "x"|"y".

type Point = { x: number; y: number; };
type P = keyof Point;

Typeof Type Operators

TypeScript ass a typeof operator you can use in a type context to refer to the type of a variable or property:

let s = "hello";
let n: typeof s;
function f() {
return { x: 10, y: 3 }
}
type P = ReturnType<typeof f>;

Indexed Access Types

We can use indexed access type to look up a specific property on another type:

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // type Age = Number

Conditional Types

At the heart of most useful programs, we have to make decisions based on input. Conditional types help describe the relation between the types of inputs and outputs.

Conditional Types

Mapped Types

Sometimes a type needs to be based on another type. Mapped types build on the syntax for index signatures, which are used to declare the types of properties which have not been declared ahead of time: A mapped type is a generic type which uses a union of PropertyKeys to iterate through keys to create a type:

type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
}

Template Literal Types

Template literal types build on string literal types, and have the ability to expand into many strings via unions.

Classes


TypeScript offers full support for the class keyword introduced in ES2015. A field declaration creates a public writeable property on a class:

class Point {
x: number;
y: number;
}

Fields can also have initializers; these will run automatically when the class is instantiated.

class Point {
x = 0;
y = 0;
// Not initialized, but no error
name!: string;
// Prevents assignments to the field outside the operator
readonly position: string = "start";
}

Just as in JavaScript, if you have a base class, you'll need to call super(); in your constructor body before using this. members.

Classes can also have accessors.

class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}

Special rules for accessors:

  • If get exists but no set, the property is automatically readonly
  • If the type of the setter parameter is not specified, it is inferred from the return type of the getter

You can use an implements clause to check that a class satisfies a particular interface

interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}

Classes may extend from a base class. A derived class has all the properties and methods of its base class, and can also define additional members.

You can use TypeScript to control whether certain methods or properties are visible to code outside the class.

The default visibility of class members is public. A public member can be accessed anywhere. protected members are only visible to subclasses of the class they're declared in. private is like protected, but doesn't allow access to the member even from subclasses.

Classes may have static members. These members aren't associated with a particular instance of the class. Static members can also use the same public, protected, and private visibility modifiers.

Static blocks allow you to write a sequence of statements with their own scope that can access private fields within the containing class. This means that we can write initialization code with all the capabilities of writing statements, no leakage of variables, and full access to our class's internals.

class Foo {
static #count = 0; 
get count() {
return Foo.#count;
} 
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
} catch {}
}
}



Modules


In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module. Conversely, a file without any top-level import or export declarations is treated as a script whose contents are available in the global scope. Modules are executed within their own scope, not in the global scope. This means that variables, functions, classes, etc. declared in a module are not visible outside the module unless they are explicitly exported during one of the export forms. To consume a variable, function, class, interface, etc. exported from a different module, it has to be imported using one the import forms.

The JavaScript specification declares that any JavaScript files without an import declaration, export, or top-level await should be considered a script and not a module. Inside a script file variables and types are declared to be in the shared global scope, and it's assumed that you'll either use the outFile compiler option to join multiple input files into one output file, or muse multiple <script> tags in your HTML to load these files.

Module resolution is the process of taking a string form the import or require statement and determining what file that string refers to.

There are two options which affect the emitted JavaScript output:

  • target which determined which JS features are down-leveled (converted to run in older JavaScript runtimes) and which are left intact
  • module which determines what code is used for modules to interact with each other


Comments

You have to be logged in to add a comment

User Comments

Frank Frank

Test of the comment trigger function

1
Frank Frank

New test of the comment trigger functionality

1
Frank Frank

Testing the comment trigger functionality again

1
Frank Frank

Testing the comment trigger functionality for the fourth time

4 GIF

1

Insert Math Markup

ESC
About Inserting Math Content
Display Style:

Embed News Content

ESC
About Embedding News Content

Embed Youtube Video

ESC
Embedding Youtube Videos

Embed TikTok Video

ESC
Embedding TikTok Videos

Embed X Post

ESC
Embedding X Posts

Embed Instagram Post

ESC
Embedding Instagram Posts

Insert Details Element

ESC

Example Output:

Summary Title
You will be able to insert content here after confirming the title of the <details> element.

Insert Table

ESC
Customization
Align:
Preview:

Insert Horizontal Rule

#000000

Preview:


View Content At Different Sizes

ESC

Edit Style of Block Nodes

ESC

Edit the background color, default text color, margin, padding, and border of block nodes. Editable block nodes include paragraphs, headers, and lists.

#ffffff
#000000

Edit Selected Cells

Change the background color, vertical align, and borders of the cells in the current selection.

#ffffff
Vertical Align:
Border
#000000
Border Style:

Edit Table

ESC
Customization:
Align:

Upload Lexical State

ESC

Upload a .lexical file. If the file type matches the type of the current editor, then a preview will be shown below the file input.

Upload 3D Object

ESC

Upload Jupyter Notebook

ESC

Upload a Jupyter notebook and embed the resulting HTML in the text editor.

Insert Custom HTML

ESC

Edit Image Background Color

ESC
#ffffff

Insert Columns Layout

ESC
Column Type:

Select Code Language

ESC
Select Coding Language

Insert Chart

ESC

Use the search box below

Upload Previous Version of Article State

ESC