TypeScript Introduction
Summary
- TypeScript (TS) is a language by Microsoft which compiles down to any version of JavaScript (JS / ES).
- TS’s main value proposition is the delivery of compile-time insights over typed libraries, values and functions. For example, TS may evaluate whether a long chain of numerical functions might suddenly return strings instead, or whether a foreign library has some method available.
- TS only works during compile-time and is fully removed during compilation. TS doesn’t run on major browsers, after all.
- It is a strict extension of JS and thus all valid JS is also valid TS, and you can use TypeScript on a pure vanilla JavaScript project for the extra IDE support.
- The IDE is the best way to consume TypeScript insights, especially through Visual Studio Code.
TypeScript productivity traps
- The TypeScript language had to evolve quickly with JavaScript, and that involved some historical bets, some of which are now retrospectively regrettable. Avoid any TS which departs from JS as they are historical artifacts. For example, the
privatekeyword is now expressed in official JS with#instead. - TypeScript can easily allow you to express something elegantly while having poor ability to assure itself that something is right; for example, the assertion that an array is never empty.
- Don’t get stuck with TS trying to prove a simple statement that you think is obvious. TS not only has a modicum of holes here and there, but the core TS team is also very speedy with their updates and responsive to bug reports. Use type assertions and other “hacks” to keep moving.
anyis not only necessary (because such is life) but may have subtle consequences, and is worth further reading.- As with all compiled/transpiled code, you will want source maps to know what line of compiled JS corresponds to what line of TS.
TypeScript noob tips
- TS and VSC are wonderful together. There are other IDE’s but VSC should be the baseline by which all are measured.
- TS can be configured with a
tsconfig.jsonfile at your project root. Consider not using strict mode but having null checking on. - TS assertions exist for you to tell the compiler that something is true even if it’s not obvious. Of course, this is a double-edged knife as TS will assume you know better.
Shutting off the TypeChecker
- Turn off strict mode
@ts-nocheckignores an entire file@ts-ignoreignores the next line@ts-expect-errorwill suppress errors on the next line if any, and TS will tell you if unnecessary!,!.not null
Primitive Value Types
In JavaScript, a value is either a primitive as listed below, or it is a reference to an object.
| primitive | type | example |
|---|---|---|
| String | string | "hi" |
| Number | number | 0.5 |
| Boolean | boolean | true |
| Undefined | undefined | undefined |
| Null | null | null |
| Symbol | symbol | new Symbol() |
| BigInt | bigint | 0n |
| top type | any | {}, "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.