TypeScript

TypeScript is the future of JavaScript. The future is now.

Overview

TypeScript is a statically-typed superset of JavaScript.

Being a superset means every JavaScript program is also a TypeScript program. A JavaScript program is just a TypeScript program that chooses not not use any of the “additional” features such as type declarations, type annotations, type assertions, type guards, and type aliases. See why it’s called TypeScript?

TypeScript programs are not run natively: you compile, or transpile, TypeScript code into JavaScript. The TypeScript compiler is pretty awesome: zillions of things which would cause runtime errors in JavaScript are caught by the TypeScript compiler! How so? The compiler does its best to figure out type constraints by analyzing your code. The type analyzer works insanely hard. Unlike the type checking in many other languages, TypeScript even uses control-flow analysis to narrow type constraints. It generally infers types much more extensively than say, Go, Rust, Java, or Swift, but for full benefit you’ll occasionally need to supply explicit type annotations.

In TypeScript, typing is optional.

You do not have to supply any type annotations at all. Specify as few or as many as you like.

Before we dive deep, here’s Jeff’s introduction:

Want to see more videos?

That’s not a bad idea! To start with the basics, see TypeScript - The Basics (it’s a few years old, but the basics haven’t changed). A longer video is TypeScript Crash Course; it covers using TypeScript with React near the end.

Getting Started

Ready to learn TypeScript?

Take a peek at the TypeScript home page to get oriented. The home page provides easy and direct access to the Documentation Home Page, and tutorials to get you up and running in just a few minutes, tailored to whether you are coming to TypeScript from backgrounds in JavaScript, Java/C#, the functional programming world, or are completely new to programming.

But if you want to jump right in, go to the TypeScript Playground. Enter this program:

greeter.ts
interface Person {
  title: string;
  name: string;
}

function greeting(salutation: string, person: Person) {
  return `${salutation}, ${person.title} ${person.name}`;
}

let user = { title: "Dr.", name: "Jane User" };

console.log(greeting("Hi", user));

Hover over the words in the editor and marvel how much TypeScript knows about your code!

Now, make some changes to the program, and marvel at how many would-be-run-time-errors in JavaScript are caught by the compiler! Some changes you may wish to try include:

Can you think of more errors that TypeScript might be able to detect before running the code?

Freedom

Some folks think JavaScript gives you more “freedom” since you can get away with crazy things and run whatever you want, without having your code rejected with lectures about how it “violates various language constraints.”

But, hang on, if you do get your JavaScript program working somehow, how “free” do you feel to make substantial changes? You might be too scared to change it. TypeScript gives you the freedom to extend, modify, and refactor your code. The compiler’s constraint checks not only save your butt, preventing costly run-time errors from ever happening, but also gives you the freedom to develop more and experiment more.

Learning TypeScript

Here are some suggestions to ramp up:

  1. After getting acquainted with the language home page, and dabbling in the Playground, head over to the documentation page.
  2. In the getting started section of the documentation page, pick the quick introduction closest to your background (but feel free to read more than one of them).
  3. Go back to the TypeScript Playground and click on the Examples dropdown. There’s a “JavaScript” section with some basic examples. Load and interact with as many of these as you can.
  4. Next, go to the TypeScript Handbook. You can read the whole thing from beginning to end quickly without stopping to try things out, then interact with it on a second pass. Or jump around, whatever works.
  5. Go back to the TypeScript Playground and again click on the Examples dropdown, but this time go to the advanced examples in the “TypeScript” section. Load and interact with as many of these as you can.

If you like books:

And here’s something else you can do. Keep reading this page of notes. Here you’ll find some language highlights, some reference-like material, and some examples. The note here, though, assume some knowledge of JavaScript, so if you feel like learning JavaScript first, you can browse this page of notes first.

Running TypeScript

While the TypeScript Playground is great for, you know, playing around, the general workflow is to write TypeScript programs in a text editor (Visual Studio Code is probably preferred in TypeScript land), then use the tsc program to translate TypeScript to JavaScript, then run your JavaScript however you like.

To install TypeScript, ensure you already have a recent version of Node.js and npm on your machine, and invoke

npm install -g typescript

Here’s a console program:

greeter.ts
interface Person {
  title: string;
  name: string;
}

function greeting(salutation: string, person: Person) {
  return `${salutation}, ${person.title} ${person.name}`;
}

let user = { title: "Dr.", name: "Jane User" };

console.log(greeting("Hi", user));

Running tsc produces a JavaScript program, so a nice way to run TypeScript is like this:

$ tsc --strict greeter.ts && node greeter.js
Hi, Dr. Jane User

Here’s a useful thing to know during development. When writing TypeScript applications, go to your project folder and run tsc --init. This creates the file tsconfig.json. Most of the defaults are good, but feel free to use a more modern version for the target property. If you don’t want to keep your .ts and .js files in the top-level folder of the project, set the rootDir and outDir properties. With the tsconfig.json file all set up, you can run:

More Sample Programs

Let’s get acquainted with what TypeScript programs look like. Don’t worry what they do; just form a mental picture of what TypeScript tends to look like.

99.ts
function collection(item: string, n: number) {
  return `${item}${n === 1 ? '' : 's'}`
}

function capitalize(s: string): string {
  return s.charAt(0).toUpperCase() + s.slice(1)
}

function bottles(n: number): string {
  const description: number | string = n === 0 ? 'no more' : n
  return `${description} ${collection('bottle', n)} of beer`
}

function onWall(n: number): string {
  return `${bottles(n)} on the wall`
}

function take(n: number): string {
  return n > 0 ? 'Take one down and pass it around' : 'Go to the store and buy some more'
}

for (let i = 99; i >= 0; i -= 1) {
  console.log(`${capitalize(onWall(i))}, ${bottles(i)}.`)
  console.log(`${take(i)}, ${onWall(i === 0 ? 99 : i - 1)}.`)
  console.log()
}
queue.ts
class Queue<T> {
  #data: T[] = []
  enqueue(item: T) {
    this.#data.push(item)
  }
  dequeue() {
    return this.#data.shift()
  }
  get size() {
    return this.#data.length
  }
}

const q = new Queue<number>()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
console.log(q.size)
console.log(q.dequeue())
top_ten_scorers.ts
import * as readline from "readline"
const reader = readline.createInterface(process.stdin)

interface Player {
  name: string
  team: string
  ppg: number
}
const players: Player[] = []

for await (const line of reader) {
  let [team, name, games, points] = line.split(",")
  if (Number(games) >= 15) {
    players.push({ name, team, ppg: Number(points) / Number(games) })
  }
}
const topTen = players.sort((p1, p2) => p2.ppg - p1.ppg).slice(0, 10)
for (let { name, team, ppg } of topTen) {
  console.log(
    `${name.padEnd(22)}${team.padEnd(4)}${ppg.toFixed(2).padStart(8)}`
  )
}

Two Kinds of Types

We have the sense that values have, or belong to, types. A technical definition of what a type is can wait; for now, let’s with our intuition. But, here’s an important concept: in TypeScript you have static types and dynamic types.

Dynamic Types

The dynamic types are those available to you at run time. There are only eight of them, all shown here:

The values......has dynamic type
35.9882
83.9e-55
Number
5n
9812983129831983192313182389n
BigInt
true
false
Boolean
"false"
"😎🥑πABC🌮🛹🤿🎥"
String
Symbol("oh no")
Symbol()
Symbol
undefinedUndefined
nullNull
{}
[]
[5, "dog"]
{x: 1, y: 2}
new Date(2023,1,1)
/(not)+ found/
Object

These are called dynamic types because they are available to you at run time. You can use the typeof operator, which sometimes works, but it hallucinates a function type that does not exist, and loses its mind when asked about null:

typeof undefined      // 'undefined'
typeof null           // 'object'   -- Lost its mind: null is not an object
typeof false          // 'boolean'
typeof 93.8888        // 'number'
typeof "Hi"           // 'string'
typeof 8n             // 'bigint'
typeof {x: 1, y: 2}   // 'object'
typeof [1, 2, 3]      // 'object'   -- sensible, arrays are just objects
typeof /o*h/          // 'object'   -- sensible, regexes are just objects
typeof (x => x + 1)   // 'function' -- Hallucination: functions have the type object
typeof NaN            // 'number'   -- sounds hilarious but makes sense

You can also use the constructor property, but there’s a lot behind the scenes to it: Booleans, numbers, bigints, strings, and symbols are internally converted into objects to make this work, (2) this won’t work for null and undefined, nor should it, and (3) the value produced might look like a type object, and you can certainly pretend it is, but it’s really a function object:

false.constructor            // Boolean
(3).constructor              // Number
95n.constructor              // BigInt
"hello".constructor          // String
Symbol('Dog').constructor    // Symbol
/a*b/.constructor            // RegExp
isFinite.constructor         // Function
[1,2,3].constructor          // Array
{x: 3, y: 5}.constructor     // Object
(new Date()).constructor     // Date
class Dog {};
d = new Dog();
d.constructor                // Dog
undefined.constructor        // THROWS A TypeError
null.constructor             // THROWS A TypeError

Static Types

Before your program is run, TypeScript will compute (or assign) types to every expression and check that they are used consistently. The types used during compilation are called static types. And the set of static types is far, far, richer than those that are available at run time. The “type” in TypeScript refers to the power of the language’s static type system.

Before looking at the big list, here’s a look at them in runnable code:

basics.ts
let n: number = 3
let b: bigint = 21n
let s: symbol = Symbol('hello')
let message: string = 'hello'
let isDone: boolean = false
let nothing: null = null
let point: object = { x: 5, y: 3 }

class Point {
  constructor(public x: number, public y: number) {}
}

point = new Point(-5, -3)
let otherPoint: Point = new Point(5, 3)
// otherPoint = point would be an error, do you see why?

let a: number[] = [5, 8, 13]
let t: [number, string] = [5.5, 'hello']

let u: 1 | 2 | 3 | boolean = 2
u = true
// u = 5 is an error

let f = (x: number) => 3 ** x
// Inferred type of f is (number)=>number

n = f(13)
let g: (z: number) => number = f

Continuing in the learn-by-example mode, now is a great time to read and study Axel Rauschmayer’s TypeScript Essentials.

Exercise: Seriously, read that whole book chapter.

Now, here is a tubular guide to most (if not all) of the static types. Note that the system is built on the idea of basic types and operators to make new types from existing ones. Occasionally you will see type variables, traditionally given with capital letters:

TypeDescription
Literal TypesAny value represents a type that consists only of that single value. For example, each of the following are actually types: 42, false, -9.5, "dog", undefined, and null. Don’t get confused between the value 42 and the type 42. Don’t get confused between the value undefined and the type undefined.
Numeric TypesThe static types number and bigint correspond to the dynamic types Number and BigInt, respectively.
The String TypeThe static type string corresponds to the dynamic type String.
The Symbol TypeThe static type symbol corresponds to the dynamic type Symbol.
The Object TypeThe static type object corresponds to the dynamic type Object, that is, the type of all objects.
Object TypesThe type expression $\{p_1: T_1, \ldots, p_n: T_n\}$ describes objects with properties $p_i$ and types $T_i$.
Interface TypesAn interface definition defines a type describing objects with certain properties. They are very similar to object types. There is so much to say about the differences between object types and interface types that we’ll have to cover that later.
Class TypesDefining a class creates a static type with the same name as the class. Objects created with the class’s constructor will have that type.
Union TypesThe type $T\,|\,U$ contains all the values of $T$ and all the values of $U$. Examples:
  • "north" | "east" | "south" | "west"
  • number | null
  • true | false
The latter is known by the special name boolean. It is common to simulate optionals with union types such as string | null.
Tuple TypesThe type $[T_1, \ldots T_n]$ is the type of all tuples of length $n$ in which the first element has type $T_1$, the second element has type $T_2$, and so on.
Array TypesThe type $T\mathtt{[]}$, which you can also write $\mathtt{Array<}$T$\mathtt{>}$, is the type of all arrays, of any length, in which all elements have type $T$.
Intersection TypesThe type $T$ & $U$ contains all the values that are both values of $T$ AND $U$.
Function TypesThe type $(p_1:T_1,\ldots,p_n:T_n) \Rightarrow U$ represents all functions with the given parameters and return type.
The Bottom TypeThe type never has no values. No value has type never. Use this type to mark functions that don’t return anything because they always throw or always run forever.
The Top TypeAll values have type unknown. Note that if a variable is constrained to unknown, it can be safely assigned anything. The converse is not true, you cannot use something of type unknown where a value of a specific type is expected.
voidUsed to mark functions that reach the end of their executing without ever returning or throwing, and thus return undefined. Saying the function is a “void function” is more accurate than saying “this function returns the value undefined” because, really, the intent for these kinds of functions is never to use their return value at all.
anyThis is not technically speaking a type, but rather a marker that says “Don’t do any type checking here!”
Exercise: I left enumerations out of this table. Why?

Type Checking

What is type checking?

Type checking is ensuring that the type of an expression is legal in the context in which it appears. What does this mean? Basically:

When an expression of type T
  • is assigned to a variable “of” type U
  • is passed to a parameter “of” type U
  • is returned from a function “of” type U
  • is assigned to a property “of” type U

...then the type checker will make sure that this makes sense.

If type checking is done at run-time, this is called dynamic type checking. If done by the compiler (before running), this is called static type checking.

any

The “type” any is just a big wildcard that complicates things, or simplifies things, depending on your point of view. It just turns off static checking, allowing for the possibility of run time type errors.

In the remainder of these notes, we’re just going to assume that we never use any and that we have all possible checks turned on.

Type Inference

TypeScript does a pretty good job of inferring type constraints. It can infer constraints for variables when their declarations have initial values, or when they are assigned to. It can infer the return types of functions.

The inferred type is not the most specific. For example:

let x = 3;          // x is inferred to have type number, not 3
let b = true;       // b is inferred to have type boolean, not true
let a = [1, 2, 3];  // a is inferred to have type number[], [number, number, number]

But tuples can still be inferred; for example, the entries method on arrays and objects yields tuple types:

TODO

TypeScript generally doesn’t infer parameter types, unless they’re given defaults.

TODO

Inference takes control flow into account, resulting in narrowing:

TODO - control flow

Object Types

TODO

TODO - object types vs interface types

Generics

TODO - talk about variance

Advanced Types

TODO - so many here

Summary

We’ve covered:

  • What TypeScript is
  • Static and Dynamic Types
  • Overview of the static types
  • Advanced features