Kazoo Engineering Blog

Thoughts and musings on building a platform from the Kazoo engineering team.

10 Weird Yet Surprisingly Useful TypeScript Types

Posted at — Oct 1, 2021

623K lines into our TypeScript journey, we’ve run into enough problems with resorting to any that we’ve put in the extra effort to type the harder stuff at the core of our platform. Read on for some of the more exotic types we’ve discovered and why you might want to use them in your own projects!

1. Json

If it type checks, you can serialize it. We use this type as a constraint anywhere we serialize JSON over the wire, whether it’s a background task payload, a pubsub message, or metadata we store in Postgres.

type Json =
  | undefined
  | null
  | boolean
  | number
  | string
  | Json[]
  | { [key: string]: Json };

See the improved support for recursive type aliases in TypeScript 3.7 that made this possible.

2. DeepPartial

For all your stubbing needs. We use this frequently in tests where we only need partial objects but want type safety for nested types!

export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends Array<infer U>
    ? Array<DeepPartial<U>>
    : T[P] extends ReadonlyArray<infer U>
    ? ReadonlyArray<DeepPartial<U>>
    : DeepPartial<T[P]> | T[P];
};

Credit: TypeORM.

3. DeepReadonly

Zero-cost immutability.

export type DeepReadonly<T> = T extends Function
  ? T
  : T extends ReadonlyArray<infer R>
  ? ReadonlyArray<DeepReadonly<R>>
  : T extends Map<infer K, infer V>
  ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
  : T extends object
  ? { readonly [P in keyof T]: DeepReadonly<T[P]> }
  : T;

Credit: https://github.com/microsoft/TypeScript/issues/13923#issuecomment-488160343

4. Paths

This type lets us create a clean DX for certain nested keys.

/**
 * This type takes an object `T` and traverses `D` paths deep to create a full dot-notation map of its keys.
 * The depth here is chosen arbitrarily to avoid downstream warnings
 * where TS detects possible infinite-depth type instantiation.
 */
type Paths<T, D extends number = 6> = [D] extends [never]
  ? never
  : T extends Array<infer ArrayType>
  ? "0" | Join<"0", Paths<ArrayType, Prev[D]>>
  : T extends object
  ? {
      [K in keyof T]-?: K extends string | number
        ? `${K}` | Join<K, Paths<T[K], Prev[D]>>
        : never;
    }[keyof T]
  : "";

/**
 * Joins key K to path P. E.g., `Join<"mama", "yo"> = "yo.mama"`
 */
export type Join<K, P> = K extends string | number
  ? P extends string | number
    ? `${K}${"" extends P ? "" : "."}${P}`
    : never
  : never;

/**
 * When doing depth-based types (e.g., Paths, below), TS will go infinitely
 * deep if you let, leading to errors. This technique prevents that by making
 * sure TS only goes `n` levels deep, where `n` is the number of elements in
 * the below tuple.
 */
export type Prev = [never, 0, 1, 2, 3, 4, 5, 6, ...0[]];

And it works like this:

// type Test = "foo" | "foo.bar" | "foo.bar.baz"
type Test = Paths<{
  foo: {
    bar: {
      baz: number;
    };
  };
}>;

5. Camelize and friends

For making APIs with different naming conventions play nice together. We turn everything into camelCase in our API gateway for consistency, and now we can do that in a type-safe way.

type CamelKey<T extends string | number | symbol> =
  T extends `${infer A}_${infer B}`
    ? CamelKey<`${A}${capitalize(B)}`>
    : T extends string ? `${uncapitalize(T)}` : T;

type Camelize<T> =
  T extends Record<string | number | symbol, unknown>
    ? { [K in keyof T as CamelKey<K>]: Camelize<T[K]> }
    : T;

And it works like this:

type Result = Camelize<{
  my_profile: {
    created_at: "2020-01-01";
    is_active: false;
  };
  Weird_API: {
    HasWeirdFields: {
      123: 456;
      true: false;
    };
  };
}>;

// type Result = {
//   myProfile: {
//       createdAt: "2020-01-01";
//       isActive: false;
//   };
//   weirdAPI: {
//       hasWeirdFields: {
//           123: 456;
//           true: false;
//       };
//   };
// }

Works for PascalCase too!

6. MultipleParameters

For inferring signatures of overloaded functions.

The built-in Parameters type has a surprising and unintuitive limitation:

function isTruthy(value: boolean): boolean;
function isTruthy(value: string): string;
function isTruthy(value: any) {
  return Boolean(value);
}

// Incorrect!
type Test = Parameters<typeof isTruthy>; // [value: string]

As of TypeScript 4.4, the only known way to infer multiple signatures is to infer a signature for each number of compatible overrides:

type MultipleParameters<T extends Fn> = T extends {
  (...args: infer A1): any;
  (...args: infer A2): any;
  (...args: infer A3): any;
}
  ? A1 | A2 | A3
  : T extends { (...args: infer A1): any; (...args: infer A2): any }
  ? A1 | A2
  : T extends { (...args: infer A1): any }
  ? A1
  : never;

type Fn<TArgs extends any[] = any[]> = (...args: TArgs) => any;

Where would we ever use this inference? On to the next item!

7. Thunk

An interface for calling a generic function with deferred arguments.

Types are correctly inferred for up to 3 overloaded signatures of fn, but you may need to pass a custom callback with narrower argument types if TSC gives you a type error for something you expected to check.

export interface Thunk {
  <T extends Fn>(fn: T, ...args: MultipleParameters<T>): () => ReturnType<T>;
}

type Fn<TArgs extends any[] = any[]> = (...args: TArgs) => any;

In use:

const toThunk: Thunk =
  (fn, ...args) =>
  () =>
    fn(...args);

8. UnionToIntersection

You have a union. You want the intersection of all the unioned types. Just read this excellent post:

https://fettblog.eu/typescript-union-to-intersection/

9. Resolvable

Say you have a parent type in GraphQL that has some specific data that’s useful for resolving one its subfields. This keeps you from easily splitting the resolver out because you lose access to the key input data!

To make things more complicated, this field can be expensive, so you want to make sure it’s resolved lazily when a consumer really needs it.

TL;DR, if you’re using Apollo server it allows you to safely resolve callbacks as GraphQL field-level resolvers — inline with the parent level resolver.

type Resolvable<T> = {
  [K in keyof T]: T[K] extends object
    ?
        | T[K]
        | (() =>
            | T[K]
            | Promise<T[K]>
            | Resolvable<T[K]>
            | Promise<Resolvable<T[K]>>)
        | Resolvable<T[K]>
    : T[K] | (() => T[K] | Promise<T[K]>);
};

We hook it into our GraphQL codegen like so:

config:
  mappers:
    GoalPermissions: Resolvable<GoalPermissions>

10. ExactlyOne

The Highlander of types.

/**
 * Given a record T, only allow instances of Partial<T> with a single key.
 *
 * @example
 *
 * // Yields exactly `{ foo: string }`
 * ExactlyOne<{ foo: string; bar: number }, "foo">
 *
 * @example
 *
 * // Yields `never`
 * ExactlyOne<{ foo: string; bar: number }, "foo" | "bar">
 */
export type ExactlyOne<T, K extends keyof T> = Exactly<
  Singles<keyof T>,
  Pick<T, K>
>;

type Singles<T extends PropertyKey> = T extends infer U
  ? { [P in U extends PropertyKey ? U : never]: unknown }
  : never;

type Exactly<T, P> = T extends {}
  ? P & { [K in Exclude<keyof P, keyof T>]?: never }
  : never;