The TypeScript Handbook
I want to read through "The TypeScript Handbook" so that I can improve my knowledge of TypeScript and define cleaner types.
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.
- You can create an object with an inferred type which includes
name: string
andid: number
const user = {
name: "Hayes",
id: 0,
};
- You can explicitly describe this object's shape using an
interface
declaration
interface User {
name: string;
id: number;
}
- 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.):
string
number
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
.
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.
- Type aliases may not participate in declaration merging, but interfaces can
- Interfaces may only be used to declare the shapes of objects, not rename primitives
- Interface names will always appear in their original form in error messages, but only when they are used by name
- Using interfaces with
extends
can often be more performant for the compiler than type aliases with intersections
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.
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.
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;
}
interface
s can also extend multiple types:
interface Colorful { color: string }
interface Circle { radius: number }
interface ColorfulCircle extends Colorful, Circle {}
interface
s 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.
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 PropertyKey
s 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 noset
, the property is automaticallyreadonly
- 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
Test of the comment trigger function
New test of the comment trigger functionality
Testing the comment trigger functionality again
Testing the comment trigger functionality for the fourth time