vars.io

TypeScript Introduction


Summary

TypeScript productivity traps

TypeScript noob tips

Shutting off the TypeChecker

Primitive Value Types

In JavaScript, a value is either a primitive as listed below, or it is a reference to an object.

primitivetypeexample
Stringstring"hi"
Numbernumber0.5
Booleanbooleantrue
Undefinedundefinedundefined
Nullnullnull
Symbolsymbolnew Symbol()
BigIntbigint0n
top typeany{}, "hi", 0.5

Annotating Types

In TS you can either annotate values directly like:

const s1 = 10n
const s2 = 20n
const sum = (a:bigint, b:bigint) => a + b

sum(s1, s2)

Or we can use type aliases which are custom types with an arbitrary name that we choose. A value which inhabits a type must match the type exactly — nothing more and nothing less.

type States = "California" | "Texas" | "New York"

function switchByState(state:States) {
   switch(state) {
      case "California":
      case "Texas":
      // "Oregon" is not compatible with <States>
      case "Oregon":
   }
}

Structural Types

Array

type tuple = [number, number]
type vector = number[]
type matrix = vector[]

const coordinate:tuple = [23, 15]
const v:vector = [1, 2, 3]
const m:matrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

Object Type

Recall that a value which inhabits a type must match the type exactly.

type Node = {
   value : number
   next  : Node | undefined
}

type LinkedList {
   root: Node | undefined
   size: number
}

// TS will reject this as the return type does not exactly match <Node>
function Node(value, next?): Node {
   return { value, next, extra: 0 }
}

Interface

A value which inhabits an interface must at least match the the properties on the interface, but may have extra properties which are unaccounted for. This is useful for documenting API’s where you are depending only on some properties.

interface AppleResponse {
   deviceType: "iPad" | "iPhone"
   availableStores: string[]
   timestamp: Date
}

Any, Unknown, and Never

The top type in most type systems is one which contains all types within a system, and may be inhabited by any value. In TS this is denoted as any. Values which cannot be inferred by TS will be assigned any. This type can “turn off” type checking in the sense that TS will infer any operation you do on such a type to be valid.

function anythingGoes(anything:any) {
   // does <anything> even have a slice() method?
   // TS will assume you know better
   return any.slice()
}

There’s also a new top type called unknown, which is like a strict version of any. TS will insist that you prove properties of an unknown type prior to using it, such as:

function unknownDataFromUser(data: unknown) {
   if (typeof data === "string") return data.concat("hello")
   if (data instanceof Array)    return data.slice()
}

The bottom type in most type systems is one which may not be inhabited by any value, and in TS this is denoted as never. Never may be used to denote a function which never returns, such as one which always throws an error.

Type Inference

Implicit Typing

let i = 10  // TS narrowly assumes that i:number
i = "moo"   // TS will bark at you

TypeScript will generally assume the narrowest type, so be careful with implicit typing:

const arr = [] // TS assumes this array is <always> empty!
arr.push(23)

Explicit Typing

let i:number|string = 10  // we accept number or string
i = "moo"                 // TS is okay with this
i = 23                    // TS is okay with this
i = 0n                    // TS will bark at you now

Generics (Type Variables)

For some functions and classes we might not want to specify the type of the parameter or return value. For example,

function logThenReturn<T>(value:T) {
   console.log(value)
   return value
}

We could type logThenReturn as a function on, say, number. But then we might be tempted to write many tiny variations of the this function for string, boolean, etc., just to get a function typing which doesn’t inappropriately broaden types within our codebase.

If we wrote the above function with type any, then anyone who uses this function may experience an inappropriate widening of types, in essence erasing their prior types.

Also consider data structures and their methods:

class Stack<T> {
   #items:T[] = []
   push(item:T) { this.#items.push(item) }
   pop(): T { return this.#items.pop() }
}

If the above Stack did not have a type parameter, then we would have difficulty informing any consumer of pop just what kind of value to expect.