Kyle Edwards

TypeScript Basics

Type System

TypeScript’s type system is unlike most other strictly typed languages you have probably dealt with because of two features:

There’s a type hierarchy that span from the infinitely wide “top type” any (or unknown) to the infinitely narrow “bottom type” never. In between, types can be declared as top-level types like number and string, or collection types as follows:

let arr: number[];
let mapping: { [field: string]: number };
let mapping2: { specificField: number; anotherField: string };

However typing can also get very specific:

const literal = "type literal"; // Type is actually "type literal."
function stringToNumber(value: "one" | "two"): number {
    let output: number;
    if (value === "one") {
        output = 1;
    } else {
        // `value` is guaranteed to be "two"
        output = 2;
    }
    return output;
}

You cannot initialize an empty array without specifying a type.

You can define “tuples” as fixed length arrays with mixed types like let tup: [number, string, boolean] = [2, "one", false]; Labeled tuple types allow you to formalize tuples and provide meaningful labels with autocompleting like so:

type PhoneNumber = [
    countryCode: number,
    areaCode: number,
    number: number
];

Types can be optional using a ?: { optField?: string }.

Intersections and Unions

interface TypeA {
    sharedField: string
    aThing: number
}

interface TypeB {
    sharedField: string
    bThing: number
}

let x: TypeA | TypeB;

In the above example, the compiler can assume that x either conforms to TypeA or TypeB, but because of this, it will not let you use .aThing or .bThing because there is no guarantee. There are ways to perform dynamic type checks, type narrowing, or casting to allow you to use them.

let y: TypeA & TypeB = {
    sharedField: "shared",
    aThing: 1,
    bThing: -9999,
};

Built-in Types

Partial<T>
Pick<T, "prop1" | "prop2" | "etc...">
Extract<>
Exclude<>

Functions

Function signatures can be separated from implementations to support “overloading” and provide specific combinations of variable types. In a way this is kind of like pattern matching in other languages.

// Signatures
function addUp(type: "number", ...rest: number[]): number;
function addUp(type: "string", ...rest: string[]): string;

// Implementation
function addUp(type: "number" | "string", ...rest: (number | string)[]): number | string {
    ...
}

You can also declare type on this in the function signature for lexical scope.

User-defined Type Guards

Sometimes the compiler won’t be able to determine that it can safely narrow a type inference, for example, in a conditional block where you’ve explicitly tested the value. In these cases, we can write function signature that hints to the compiler that it should trust that we’ve performed the check manually.

function isDefined<T>(arg: T | undefined): arg is T {
    return typeof arg !== 'undefined';
}

Types vs. Interfaces

Type aliases a generally more flexible than interfaces as they can deal with scalar and union types like type Zip = string | number;, however they cannot be self-referential until TypeScript 4.0+.

Interfaces deal exclusively with entities that extend from the JavaScript Object type, so functions, arrays, and objects are supported. They can extend from one another. Types can also handle these things. Interfaces are calculated lazily, while types are calculated

interface Dict {
    [key: string]: undefined | {
        title: string;
        body: string;
    };
}

// Interfaces can be combined.
interface Dict {
    global: {
        title: string;
        body: string;
    };
}

let x: Dict;
x.global; // Must exist!
x.other // Optionally exists...

Classes

Type Constraints

Type constraints allow for genericized type parameters that meet certain requirements necessary for the compiler. This is especially useful for differing objects or dictionaries that must have a certain field. This is a bit different from dictionary type matching in that generics still return the type as-is.

Returns for superclasses and base interfaces reduce the fidelity of the known type, which is when type parameters come in handy.

function test<T extends { field: string }>(list: T[]): T {
    ...
}

Other Topics