TypeScript

Type Aliases vs. Interfaces

Tip

When creating types for objects, prefer interfaces when possible.

Tuples

type NameAndAge = [string, number]

const person: NameAndAge = ['Bob', 42]
const [name, age] = person // destructuring
// an array with at least two numbers
type operands = [number, number, ...number[]]

// an array with a string, number, and any number of other arguments of any type
type arguments = [string, number, ...any[]]

Enums

enum Response { Yes, No }
// equivalent to
enum Response { Yes = 0, No = 1}

handleResponse(Response.Yes)
enum Response { Yes = 1, No }
// equivalent to
enum Response { Yes = 1, No = 2}
enum Color { Red = 1, Blue }

console.log(Colors.Red) // 1
console.log(Colors[1]) // "Red"
enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

console.log(Direction.Up) // "UP"
console.log(Direction["UP"]) // error
enum LogLevel {
  ERROR,
  WARN,
  INFO,
  DEBUG,
}

type LogLevelStrings = keyof typeof LogLevel
// equivalent to
type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG'

Computed members

enum Test {
    // all these are constant
    A = 1,
    B = 2,
    C = A + B, // still constant because it's evaluated at compile time
    // this is computed
    Z = "aaaaa".length
}

Const enums

const enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

let skyDirection = Direction.Up

// everything above compiles to:
let skyDirection = "UP"

Template literal types

type PlanType = 'individual' | 'family'
type PlanSchedule = 'monthly' | 'annual'

type PlanString = `${PlanType}.${PlanSchedule}`
// 'individual.monthly' | 'individual.annual' | 'family.monthly' | 'family.annual'

type PlanStringCamelCase = `${PlanType}${Capitalize<PlanSchedule>}`
// 'individualMonthly' | 'individualAnnual' | 'familyMonthly' | 'familyAnnual'

satisfies

type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
const palette: Record<Colors, string | RGB> = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255] // this typo will be caught
};

const redComponent = palette.red.at(0); // but this will error, because the type of palette.red has changed to string|RGB
const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255] // the typo is caught
} satisfies Record<Colors, string | RGB>;

// these both work because the properties keep their types
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();

Narrow type of for...in loop key

const Colors = {
    red: '#ff0000',
    green: '#00ff00',
    blue: '#0000ff',
}

let key: keyof typeof Colors
for (key in Colors) {
    console.log(`The hex code of ${key} is ${Colors[key]}`)
}

Arrow functions with generics

const arrowFunc = <T,>(value: T) => { /* ... */ }

Function return type based on parameters

enum SelectionKind {
    Single,
    Multiple,
}

interface QuickPickReturn {
    [SelectionKind.Single]: string;
    [SelectionKind.Multiple]: string[];
}

async function showQuickPick<S extends SelectionKind>(
    prompt: string,
    selectionKind: S,
    items: readonly string[],
): Promise<QuickPickReturn[S]> {
    // returns String if SelectionKind is Single, and String[] if SelectionKind is Multiple
}

Function overloads

interface Fruit { /* ... */ }
interface Apple extends Fruit { /* ... */ }

function getFruits(options?: { applesOnly: false }): Fruit[]
function getFruits(options: { applesOnly: true }): Apple[]
function getFruits({ applesOnly = false } = {}): Fruit[]  {
    /* function body here */
}

// type Fruit[]
const fruitSaladIngredients = getFruits()
// type Fruit[]
const tartIngredients = getFruits({ applesOnly: false })

// type Apple[] - even though the implementation signature declares a return type of Fruit[], TypeScript knows to narrow it based on the overload
const applesauceIngredients = getFruits({ applesOnly: true })

Discriminating unions

type NetworkLoadingState = {
  state: 'loading'
}
type NetworkFailedState = {
  state: 'failed'
  code: number
}
type NetworkSuccessState = {
  state: 'success'
  response: NetworkResponse
}

type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState
  | NetworkSuccessState

function logState(requestState: networkState) {
    // requestState.state is safe to access because it exists
    // on every type in the union 
    console.log(`Request status: $(requestState.state)`)

    switch (requestState.state) {
        case 'failed':
            // this narrows the type of requestState to NetworkFailedState
            console.log(`Error code: $(requestState.code)`)
            break
        /* ... other cases */
    }
}

infer

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
type Flat1 = Flatten<number[]> // number
type Flat2 = Flatten<string> // string

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;
type Num = GetReturnType<() => number>; // number
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // boolean[]
type NotFunction = GetReturnType<string> // never

Type Manipulation

Extending interfaces

interface Foo { a: number }
interface Bar { b: number }

interface Baz extends Foo, Bar {
  c: number
}
interface Foo { x: { y: number } }
interface Bar { x: { z: string } }

interface Baz extends Foo, Bar // error: Named property 'x' of types 'Foo' and 'Bar' are not identical

Interface declaration merging

interface Book {
  title: string
  author: string
}

interface Book {
  year: number
}

Combine types (intersection type)

type Foo = { a: number }
interface Bar { b: number }

// you can combine types (including object literals) & interfaces, and the result will be a type
type Baz = Foo & Bar & {
  c: number
}
type Foo = { x: { y: number } }
type Bar = { x: { z: string } }

type Baz = Foo & Bar

const x: Baz = { x: { y: 123, z: 'abc' } }

Combine index signatures and known properties

interface Foo {
  length: number;
}

interface Bar {
  [key: string]: string;
}

type FooBar = Foo | Bar;

const foo: FooBar = {
  length: 1, // OK
  txt: "TXT", // OK
  hello: 1 // not allowed
};

Get type of a property (indexed access types)

interface Book {
    genre: 'comedy' | 'drama' | 'mystery'
}

interface Movie {
    genre: Book['genre']
}

Remap properties from another type or union (mapped types)

interface Features {
  darkMode: () => void;
  newUserProfile: () => void;
};

// { darkMode: boolean; newUserProfile: boolean; }
type FeatureOptions = {
  [Property in keyof Features]: boolean;
}
type ColorChannel = 'red' | 'green' | 'blue' | 'alpha'

interface Color {
    /* this won't work */
    // [channel: ColorChannel]: number

    /* use this instead */
    [channel in ColorChannel]: number
}
type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
 
interface Circle {
    kind: "circle";
    radius: number;
}
 
type KindlessCircle = RemoveKindField<Circle>;
// { radius: number; }
type EventConfig<Events extends { kind: string }> = {
    [E in Events as E["kind"]]: (event: E) => void;
}
 
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };

// {
//   square: (event: SquareEvent) => void;
//   circle: (event: CircleEvent) => void;
// }
type Config = EventConfig<SquareEvent | CircleEvent>
type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
 
interface Person {
    name: string;
    age: number;
    location: string;
}

// {
//   getName: () => string;
//   getAge: () => number;
//   getLocation: () => string;
// }
type LazyPerson = Getters<Person>;

Mapping modifiers

// Removes 'readonly' attributes from a type's properties
type Mutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};
 
type LockedAccount = {
  readonly id: string;
  readonly name: string;
};
 
type UnlockedAccount = Mutable<LockedAccount>;

// Removes 'optional' attributes from a type's properties
type Concrete<Type> = {
  [Property in keyof Type]-?: Type[Property];
};
 
type MaybeUser = {
  id: string;
  name?: string;
  age?: number;
};
 
type User = Concrete<MaybeUser>;

Union type from type/interface keys (keyof)

interface Person {
    first_name: string
    last_name: string
    age: number
}

// 'first_name' | 'last_name' | 'age'
type PersonFields = keyof Person

Union type from object keys (keyof typeof)

const bob = {
    first_name: 'Bob',
    last_name: 'Jones',
    age: 27
}

// 'first_name' | 'last_name' | 'age'
type PersonFields = keyof typeof bob

Union type from object values

const bob = {
    first_name: 'Bob',
    last_name: 'Jones',
    age: 27
}

// string | number
type PersonFields = (typeof bob)[keyof typeof bob]
const bob = {
    first_name: 'Bob',
    last_name: 'Jones',
    age: 27
} as const

// 'Bob' | 'Jones' | 27
type PersonFields = (typeof bob)[keyof typeof bob]

Union type from array items

const people = [{ name: 'Bob', age: 23 }, { name: 'Susan', age: 16 }]
type Person = typeof people[number] // { name: string, age: number }

const animals = ['cat', 'dog', 'mouse'] as const

// 'cat' | 'dog' | 'mouse'
type Animal = typeof animals[number]
// 'dog'
type Dog = typeof animals[1]

Union type from property values in array of objects

const animals = [
  { species: 'cat', name: 'Fluffy' },
  { species: 'dog', name: 'Fido' },
  { species: 'mouse', name: 'Trevor' }
] as const

// 'cat' | 'dog' | 'mouse'
type Animal = typeof animals[number]['species']

Pick properties of T, omitting properties of BaseModel

type ModelFields<T> = Omit<T, keyof BaseModel>

Pick all properties of type Value from T

type PickByType<T, Value> = {
  [P in keyof T as T[P] extends Value | undefined ? P : never]: T[P]
}

Declaration files (.d.ts)

Import types

declare class Holiday {
  name: string
  date: import('dayjs').Dayjs
}

Augment window

interface Window {
    globalProperty: string
}

Extend globals (such as process.env)

declare global {
	namespace NodeJS {
		interface ProcessEnv {
			NODE_ENV: 'development' | 'sandbox' | 'production'
		}
	}
}

Configuration

TypeScript ESLint

See also