Published on

Typescript Types Cookbook

Authors
  • avatar
    Name
    Justin D Vrana
    Twitter

This post contains a continually updated list of handy Typescript recipes. These have been written, curated, and edited by me from various projects over the years.

Enjoy!


Simple types

/**
 * Unpack an inner type such as an Array<T>
 */
export type Unpacked<T> = T extends Array<infer U> ? U : T

/**
 * Recursive array (e.g. for ReactChildren)
 */
export type RecursiveArray<T> = Array<T | RecursiveArray<T>>

export type ReactChildrenOfType<P> = RecursiveArray<React.ReactElement<P>> | React.ReactElement<P>

/**
 * Ensures an array type has at least one element.
 * @example
 * let validArray: NonEmptyArray<number> = [1]; // Valid
 * let invalidArray: NonEmptyArray<number> = []; // Error
 */
export type NonEmptyArray<T> = [T, ...T[]];

/**
 * Makes selected properties of an object type optional.
 * @example
 * interface Example { a: number; b: string; }
 * type ExamplePartial = PartialBy<Example, 'b'>; // { a: number; b?: string; }
 */
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

/**
 * Extracts the keys of an object where the property is required (not undefined).
 * @example
 * interface Example { a: number; b?: string; }
 * type ExampleRequiredKeys = RequiredKeys<Example>; // 'a'
 */
export type RequiredKeys<T> = { [K in keyof T]-?: undefined extends T[K] ? never : K }[keyof T];


/**
 * Converts a readonly structure into a mutable one.
 * @example
 * const config: Writable<Readonly<{ a: number }>> = { a: 5 };
 * config.a = 10; // Now possible
 */
export type Writable<T> = { -readonly [P in keyof T]: T[P] };

/**
 * Makes every nested property of an object readonly.
 * @example
 * const nested: DeepReadonly<{ a: { b: number } }> = { a: { b: 1 } };
 * nested.a.b = 2; // Error
 */
export type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> };

/**
 * Extracts the type wrapped in a Promise.
 * @example
 * async function getValue() { return 5; }
 * type Value = PromiseType<ReturnType<typeof getValue>>; // number
 */
export type PromiseType<T extends Promise<any>> = T extends Promise<infer U> ? U : never;

/**
 * Flattens nested types into a single level object type.
 * @example
 * type Nested = { a: { b: number } };
 * type Flat = Flatten<Nested>; // { a: { b: number } }
 */
export type Flatten<T> = { [P in keyof T]: T[P] extends infer U ? U : T[P] };

/**
 * Extracts the argument types from a function as a tuple.
 * @example
 * function example(x: number, y: string): void {}
 * type Args = FunctionArgs<typeof example>; // [number, string]
 */
export type FunctionArgs<T extends Function> = T extends (...args: infer U) => any ? U : never;

/**
 * Extracts the return type of a function.
 * @example
 * function sum(a: number, b: number): number { return a + b; }
 * type SumResult = FunctionReturnType<typeof sum>; // number
 */
export type FunctionReturnType<T extends Function> = T extends (...args: any[]) => infer U ? U : never;

/**
 * Implements a type-level conditional expression.
 * @example
 * type IsString<T> = If<T extends string, 'Yes', 'No'>;
 * type Test = IsString<string>; // 'Yes'
 * type Test2 = IsString<number>; // 'No'
 */
export type If<Condition extends boolean, True, False> = Condition extends true ? True : False;

Types of paths, keys, or attributes

/**
 * Recursively get type of all keys
 */
export type RecursiveKeyOf<TObj extends object> = {
  [TKey in keyof TObj & (string | number)]:
  TObj[TKey] extends any[] ? `${TKey}` :
    TObj[TKey] extends object
      ? `${TKey}` | `${TKey}.${RecursiveKeyOf<TObj[TKey]>}`
      : `${TKey}`;
}[keyof TObj & (string | number)]

/**
 * Returns an interface stripped of all keys that don't resolve to U, defaulting
 * to a non-strict comparison of T[key] extends U. Setting B to true performs
 * a strict type comparison of T[key] extends U & U extends T[key]
 */
export type KeysOfType<T, U, B = false> = {
  [P in keyof T]: B extends true
    ? T[P] extends U
      ? (U extends T[P]
          ? P
          : never)
      : never
    : T[P] extends U
      ? P
      : never;
}[keyof T]

/**
 * Returns an interface stripped of all properties that do not resolve to U, defaulting
 * to a non-strict comparison of T[key] extends U. Setting B to true performs
 * a strict type comparison of T[key] extends U & U extends T[key]

 * For example, you may want to pick all type properties of type
 */
type PickByType<T, U, B = false> = Pick<T, KeysOfType<T, U, B>>

type OmitByType<T, U, B = false> = Omit<T, KeysOfType<T, U, B>>
/**
 * Constructs a string literal that represents a path by joining two segments with a dot ('.') if the second segment is not empty.
 * This type utility is used to concatenate path segments in a type-safe manner, ensuring that segments are only joined if both are strings or numbers.
 *
 * @typeparam K The first path segment, which must be a string or number.
 * @typeparam P The second path segment, which must be a string or number.
 * @returns A string literal representing the joined path. If `P` is an empty string, `K` is returned without a dot. Otherwise, `K` and `P` are joined by a dot.
 *
 * @example
 * type Path = Join<'a', 'b'>; // Result: 'a.b'
 * type SimplePath = Join<'a', ''>; // Result: 'a'
 */
export type Join<K, P> = K extends string | number ?
  P extends string | number ?
    `${K}${'' extends P ? '' : '.'}${P}`
    : never : never

/**
 * A utility type used to decrement a numeric type within other type definitions, primarily used in recursive type aliases to manage depth.
 * The type effectively maps a number to its predecessor, with a defined mapping for values from 0 to 20 and a fallback to `never` for deeper recursion.
 *
 * @returns The predecessor of a given index number within the range of 0 to 20, with `never` used for indices outside this range.
 *
 * @example
 * type Zero = Prev[1]; // Result: 0
 * type OutOfBounds = Prev[21]; // Result: never
 */
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
  11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...Array<0>]

/**
 * Generates a union of string literals representing all possible leaf paths through an object's properties up to a specified depth `D`.
 * This type recursively explores each branch of the object to the specified depth, capturing the paths that lead to leaf nodes (end points).
 * It is particularly useful for type-safe access to nested properties and for generating exhaustive lists of paths to leaf nodes.
 *
 * @typeparam T The object type to generate leaf paths from.
 * @typeparam D The maximum depth to traverse within the object, defaulting to 10.
 * @returns A union of string literals, each representing a path to a leaf property within object `T`.
 *
 * @example
 * interface Example {
 *   a: {
 *     b: {
 *       c: number;
 *     };
 *     d: string;
 *   };
 * }
 *
 * // Type would be "a.b.c" | "a.d"
 * type ExampleLeaves = Leaves<Example>;
 */

export type Leaves<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
    { [K in keyof T]-?: Join<K, Leaves<T[K], Prev[D]>> }[keyof T] : ''

/**
 * Generates a union of string literals representing all possible paths through an object's properties up to a
 * specified depth. Will recursively traverse up to depth `D`, defaulting to 10 levels deep.
 *
 * @typeParam T The object type to generate paths from.
 * @typeParam D The maximum depth to traverse within the object, defaulting to 10.
 * @returns A union of string literals, each representing a path to a property within object `T`.
 *
 * @example
 * interface Example {
 *   a: {
 *     b: {
 *       c: number;
 *     };
 *     d: string;
 *   };
 * }
 *
 * // Type would be "a" | "a.b" | "a.b.c" | "a.d"
 * type ExamplePaths = Paths<Example>;
 */
export type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
    {
      [K in keyof T]-?: K extends string | number ?
    `${K}` | Join<K, Paths<T[K], Prev[D]>>
        : never
    }[keyof T] : ''

Specialty Types

GraphQL for Mocked Types in Storybook

These types are for a fairly specific situation of mocking graphql responses for Storybook testing.

The finals results are fully-typed mocked graphql data for your Storybook stories. Pretty useful.

Here is the file structure for this example:

src
|── types.ts
|── gqlhub
|   |── queries.ts
|── utils
|   |── types.ts
|   |── testing.utils
|── components
|   |── FooComponent
|   |   |── FooReactComponent.tsx
|   |   |── FooReactComponent.stories.tsx
|   |   |── mocks.ts

types.ts

import { TypedDocumentNode } from '@graphql-typed-document-node/core'

export type ResultOfGraphQLNode<Type> = Type extends TypedDocumentNode<infer X> ? X : never──
export type VarsOfGraphQLNode<Type> = Type extends TypedDocumentNode<any, infer X> ? X : never

utils/testing.utils

import { ApolloError } from '@apollo/client'
import { MockedResponse } from '@apollo/client/testing'
import { ResultOfGraphQLNode, VarsOfGraphQLNode } from 'types'

/**
 * A ApolloClient mock type
 *
 * @example
 * const mocks: Array<Mock<typeof QUERY_LIST_EXPERIMENT>> = []
 */
export interface ApolloClientMock<T> {
  request: {
    query: T
    variables?: VarsOfGraphQLNode<T>
  }
  result: {
    data: ResultOfGraphQLNode<T> | Generator<ResultOfGraphQLNode<T>>
    loading?: boolean
  }
  error?: ApolloError
  delay?: number
}

// export type ApolloClientMockArray<T> = Array<ApolloClientMock<T>>
export type ApolloClientMockArray<T> = Array<MockedResponse<ResultOfGraphQLNode<T>>>

mocks.ts

import { ApolloClientMockArray } from 'utils/testing.utils'
import { GET_FOO_QUERY } from '../queries'

type MockType = ApolloClientMockArray<typeof GET_FOO_QUERY>

const mocks: MockType = [
  {
    request: {
      query: GET_FOO_QUERY
    },
    result: {
      data: {
        queryResults: {
          __typename: 'FooResult',
          results: [
            {
              __typename: 'FileUserAssociation',
              name: 'foo1'
            },
            {
              __typename: 'FileUserAssociation',
              name: 'foo2'
            },
          ]
        }
      }
    }
  }
]

export default mocks

And finally, using the mocks in a storybook:


import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import FooReactComponent from './FooReactComponent'
import { MockedProvider } from '@apollo/client/testing'
import mocks from './story.data'

const meta: Meta<typeof FooReactComponent> = {
  title: 'Tutorial/FooReactComponent',
  component: FooReactComponent
}

export default meta

type Story = StoryObj<typeof FooReactComponent>

/*
 *👇 Render functions are a framework specific feature to allow you control on how the component renders.
 * See https://storybook.js.org/docs/react/api/csf
 * to learn how to use render functions.
 */
export const Default: Story = {
  args: {},
  render: (args) => <div>
    <MockedProvider mocks={mocks} >
      <ExperimentExplorer/>
    </MockedProvider>
  </div>
}