I finally cracked and learned Typescript

Are those red squiggles and

any, any, void
starting to make you go crazy? Do you look at type signatures and feel dumber? Do all your files end in
ts
or
tsx
anyways? Have you read a software engineering job description in the past six months?

Fine, I give up, I’ll learn TypeScript.

What is it?

TypeScript is superset of features built on top of Javascript with the goal of static typechecking. It's open source and maintained by Microsoft.

Why?

Type errors (i.e. providing one type of value where another was expected) are the most common error in Javascript. These come from typos, misunderstanding API usage, incorrect runtime assumptions, or other errors—and Typescript will highlight them immediately.

Typescript does not run at runtime, it only helps at build time, sort of like a fancy linter.

Typescript has started to become a ubiquitous part of a frontend stack. TypeScript moves some kinds of common programming errors from runtime to compile time.

In Javascript, we only realize things fail at the exact moment the problematic use happens. With Typescript, we get better errors, at the invocation site.

How?

Luckily, existing Javascript code is also Typescript code. Typescript knows Javascript and will generate types automatically if none are defined.

// user.ts type User { name: string; id: number; } const user: User = { name: “Jimmy”, id: 1, }; function deleteUser(user: User) { //… }

Typescript types

Typescript extends primitive types (

boolean
,
bigint
,
null
,
number
,
string
,
symbol
,
undefined
) to include a few more:

  • any
    - allow any type
  • unknown
    - ensure type is declared by user
  • never
    - to prevent a type
  • void
    - a function that returns nothing or
    undefined
  • array
    - for an array like
    [1, 2, 3]
    simply define as
    number[]
    or
    Array<number>

It’s also possible to create custom complex types by combining simple ones.

  • union - a type that can be thought of as “OR” for types
    type MyBool = true | false;
    “It’s much more common to see unions types, I’d say in your day-to-day you’ll see 100:1 ratio—these are overwhelmingly the more common thing and it just happens to do with control flow” - Mike North, Developer Platform Lead at Stripe
  • intersection - “AND” for types: only the values that are shared by the intersecting sets
  • generics - provide parameters to types for greater reusability
  • structural type - a principle of Typescript that type checking focuses on the shape of the values, so even if a variable is never declared to be a certain type, if it contains the same shape as a declared type it will be assigned that type automatically

Annotations on variables

When declaring a variable using

const
,
var
, or
let
, you can optionally add an explicit type annotation:

let myName: string = “Jamison”;

But in most cases this is unnecessary because Typescript will infer the type automatically.

Functions

Parameter type annotations

function greet(name: string) { console.log(“Hello,+ name.toUpperCase() +!); }

Return type annotations

Typescript will infer the function’s return type based on the

return
statement but it’s still possible to annotate:

function getThirteen(): number { return 13; }

It’s beneficial to annotate return types because the type error will occur at the function level rather than where the return type is used in subsequent code.

Contextual typing

When a function appears in place where Typescript can determine how it’s going to be called, the parameters of that function are given types.

const name = [“Alice”, “Bob”, “Eve”]; //parameter s inferred to have type string names.forEach( function (s) { console.log(s.toUpperCase()) }

Void

For when we don’t care about the return value irrespective of the value being returned or not,

void
is used as the return signature. 100% side effect function.

function invokeInFourSeconds(callback: () => undefined) { setTimeout(callback, 4000); } // ‘Disregard this return value’ function invokeInFiveSeconds(callback: () => void) { setTimeout(callback, 5000); } const values: number[] = []; //! Error: Type 'undefined' is not assignable to type 'number'. // push() returns the length of the array, a number invokeInFourSeconds(() => values.push(4)); invokeInFiveSeconds(() => values.push(4));

Callables

Describe call signature type

interface TwoNumberCalculation { // return type for an interface is ‘:’ (x: number, y: number): number; } // return type for a type is ‘=>’ type TwoNumberCalc = (x: number, y: number) => number; const add: TwoNumberCalculation = (a, b) => a + b; const subtract: TwoNumberCalc = (x, y) => x - y;

Constructables

interface DateConstructor { new (value: number): Date; } let MyDateConstructor: DateConstructor = Date; // const d: Date const d = new MyDateConstructor(1697923072611);

Function Overloads

By defining multiple function heads on the same implementation, it’s possible to ‘overload’ a function, thereby allowing the second argument’s type to change automatically based on the first argument.

type FormSubmitHandler = (data: FormData) => void; type MessageHandler = (evt: MessageEvent) => void; // these are two ‘heads’ of a single function declaration // links the first and second arguments function handleMainEvent( elem: HTMLFormElement, handler: FormSubmitHandler ): void; function handleMainEvent( elem: HTMLIFrameElement, handler: MessageHandler ): void; function handleMainEvent( elem: HTMLFormElement | HTMLIFrameElement, handler: FormSubmitHandler | MessageHandler ): void {} const myFrame = document.getElementsByTagName("iframe")[0]; // const myFrame: HTMLIFrameElement const myForm = document.getElementsByTagName("form")[0]; // const myForm: HTMLFormElement handleMainEvent(myFrame, (val) => {}); /* function handleMainEvent(elem: HTMLIFrameElement, handler: MessageHandler): any (+1 overload) */ handleMainEvent(myForm, (val) => {}); /* function handleMainEvent(elem: HTMLFormElement, handler: FormSubmitHandler): any (+1 overload) */

this
type

In order to type

this
implicitly without sending whatever
this
is, prepend
this: This
to the argument list and bind the
This
to the handler.

function myClickHandler(this: HTMLButtonElement, event: Event) { this.disabled = true; } myClickHandler(new Event("click")); // lacks `this` const myButton = document.getElementsByTagName("button")[0]; const boundHandler = myClickHandler.bind(myButton); boundHandler(new Event("click")); // bound version: ok myClickHandler.call(myButton, new Event("click")); // also ok

Objects

It’s possible to annotate objects and their properties, separating them with either

,
or
;
.

function printCoordinates(pt: { x: number; y: number }) { console.log(pt.x, pt.y); }

To make a property optional, add a

?
after the property name.

function printName(obj: { first: string; last?: string }) { }

Arrays

const fileExtensions = ["js", "ts"]; // typeof fileExtensions === string[]

Arrays can also be expressed like

Array<string>
but that’s not recommended as it will clash with jsx code.

const cars = [ { make: "Toyota", model: "Corolla", year: 2002, }, ];

Infers:

type cars = { make: string; model: string; year: number; }[];

Tuples

For data structures of non-arbitrary length, it’s possible to annotate each index of the tuple separately.

let myCar: [number, string, string] = [2002, "Toyota", "Corolla"];

Add

readonly
before type declaration to prevent insertion or deletion from tuple.

Type aliases

A type alias is a name for a type.

type Point = { x: number; y: number; }; function printCoordinates(pt: Point) { console.log(pt.x, pt.y); }

Type aliases function like add-ons for existing types.

SpecialDate
will pass as a
Date
plus have the
getDescription()
property.

type SpecialDate = Date & { getDescription(): string }; const newYearsEve: SpecialDate = Object.assign(new Date(), { getDescription: () => "Last day of the year", });

Typing dynamic object keys

For object indexes or keys that need types, there’s a

Record
type.

//Allows dynamic keys to be added to cache at runtime const cache: Record\<string, string\> = {}

There’s also an index signature:

const cache: { [id: string]: string; } = {};

Which can be abstracted to its own interface:

type Cache { [id: string] : string; }

Try-Catch and Errors

In a generic try-catch statement the error destructures into an error name and message.

try { throw new TypeError(“oops”); } catch ({ name, message }) { console.log(name); // “TypeError” console.log(message); // “oops” }

For the scenarios where we’re not hardcoding the error type, it’s best practice to coerce or check the error with

instanceof
:

} catch (e) { // e: any at this point if (e instanceof Error) { return e.message; } }

Extending types

In order to keep code DRY, it’s possible to extend types with interfaces, or copy them to other named types, and add new members.

extends
is for “alike” things.

interface Animal { id: string; name: string; } interface Mammel extends Animal { hairOrFurColor: string; }

Augment interfaces

window.document; // an existing property // ^? (property) document: Document window.exampleProperty = 42; // ^? (property) exampleProperty: number // tells TS that `exampleProperty` exists declare global { interface Window { exampleProperty: number; } }

Recursive types

type NestedNumbers = number | NestedNumbers[]; const val: NestedNumbers = [3, 4, [5, 6, [7], 59], 221];

Intersection types

Combine multiple types by extending them—everything that is in both sets:

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

Omit type

Constructs a type by picking all the properties from the

Type
and removing the desired
Keys
.

interface User { id: string; firstName: string; lastName: string; } //Omit id from User type MyType = Omit\<User, "id"\>;

Narrowing with type guards

const success = [ "success", { name: "Mike North", email: "mike@example.com" }, ] as const; const fail = ["error", new Error("Something went wrong!")] as const; const [first, second] = outcome2; if (second instanceof Error) { // In this branch of your code, second is an Error // All properties of `second` are typed second; } else { // In this branch of your code, second is the user info second; }

Built-in type checks

let value: | Date | null | undefined | "pineapple" | [number] | { dateRange: [Date, Date] }; // instanceof if (value instanceof Date) { value; // ^Date } // typeof else if (typeof value === "string") { value; // ^pineapple } // Specific value check else if (value === null) { value; // ^null } // Truthy/falsy check else if (!value) { value; // ^undefined } // Some built-in functions else if (Array.isArray(value)) { value; // ^[number] } // Property presence check else if ("dateRange" in value) { value; // ^let value = { dateRange: [Date, Date] } } else { value; // ^never }

User-defined type guard

interface CarLike { make: string; model: string; year: number; } let maybeCar: any; // the guard function isCarLike(valueToTest: any): valueToTest is CarLike { return ( valueToTest && typeof valueToTest === "object" && "make" in valueToTest && typeof valueToTest["make"] === "string" && "model" in valueToTest && typeof valueToTest["model"] === "string" && "year" in valueToTest && typeof valueToTest["year"] === "number" ); } // using the guard if (isCarLike(maybeCar)) { maybeCar; // ^? let maybeCar: CarLike }

asserts value is Foo

For use when throwing errors is optimal escape

function assertsIsCarLike(valueToTest: any): asserts valueToTest is CarLike { if ( !( valueToTest && typeof valueToTest === "object" && "make" in valueToTest && typeof valueToTest["make"] === "string" && "model" in valueToTest && typeof valueToTest["model"] === "string" && "year" in valueToTest && typeof valueToTest["year"] === "number" ) ) throw new Error(`Value does not appear to be a CarLike${valueToTest}`); } assertsIsCarLike(maybeCar); // maybeCar can now be assumed to be CarLike maybeCar;

Discriminated union

It’s possible to use signals to narrow a larger value

if (first === "error") { // In this branch of your code, second is an Error second; } else { // In this branch of your code, second is the user info second; }

Type Queries

Provides a way to get type representation of all property keys of a given interface

keyOf

Object.keys()
for types

type DatePropertyNames = keyof Date; // complete set of date properties that have string keys type DateStringPropertyNames = DatePropertyNames & string;

TypeOf

Get the type from a value.

async function main() { const apiResponse = await Promise.all([ fetch("https://example.com"), Promise.resolve("Titanium White"), ]); type ApiResponseType = typeof apiResponse; // type ApiResponseType = [Response, string] }

Index Access Type

interface Car { make: string; model: string; year: number; color: { red: string; green: string; blue: string; }; } let carColor: Car["color"]; //✔️ Reaching for something that exists let carSomething: Car["not-something-on-car"]; //! Reaching for something invalid let carColorRedComponent: Car["color"]["red"]; //✔️ Reaching for something nested // project a union through an index access type let carProperty: Car["color" | "year"]; // ✔️ Passing a union type through the index

Summary

Mike North provided an excellent exercise in his Typescript 5+ Fundamentals v4 about building more complex types using index signatures, recursive types, and union types.

// create a base JSON value type JSONPrimative = boolean | string | number | null; // use index signatures to restrict keys to strings and // values to any JSON type JSONObject = { [key: string]: JSONValue }; // arrays can be an array of any JSON value type JSONArray = JSONValue[]; // combine the set and wrap it with a recursive type type JSONValue = JSONPrimative | JSONObject | JSONArray; ////// TEST SUITE BELOW ////// function isJSON(arg: JSONValue) {} // POSITIVE test cases (must pass) isJSON("hello"); isJSON([4, 8, 15, 16, 23, 42]); isJSON({ greeting: "hello" }); isJSON(false); isJSON(true); isJSON(null); isJSON({ a: { b: [2, 3, "foo"] } }); // NEGATIVE test cases (must fail) // @ts-expect-error isJSON(() => ""); // @ts-expect-error isJSON(class {}); // @ts-expect-error isJSON(undefined); // @ts-expect-error isJSON(BigInt(143)); // @ts-expect-error isJSON(isJSON);