Useful TypeScript Helpers

Published: September 7, 2023 Updated: October 12, 2023

Useful TypeScript Helpers

Table of Contents

Type Declarations

View Transition

The startViewTransition function is a method on the document object which enables view transitions. The following types are based on the documentation at MDN. Since the API is currently experimental, you may need to add the following type to your type declaration file to avoid errors.

declare global {
	interface ViewTransition {
		/**
		 * A Promise that fulfills once the transition animation is finished, 
		 * and the new page view is visible and interactive to the user.
		 */
		finished: Promise<void>;
		/**
		 * A Promise that fulfills once the pseudo-element tree is created 
		 * and the transition animation is about to start.
		 */
		ready: Promise<void>;
		/**
		 * A Promise that fulfills when the promise returned by the 
		 * document.startViewTransition()'s callback fulfills.
		 */
		updateCallbackDone: Promise<void>;
		/**
		 * Skips the animation part of the view transition, but doesn't skip 
		 * running the document.startViewTransition() callback that updates the DOM.
		 */
		skipTransition: () => void;
	}

	type ViewTransitionCallback = (() => Promise<void>) | (() => void);

	interface Document {
		startViewTransition: (callback: ViewTransitionCallback) => ViewTransition;
	}
}
Using in SvelteKit

In SvelteKit, you can add the above type declaration to a file in the src directory, such as src/types.d.ts. Then you can add view transitions to your SvelteKit app by following the video below, ignoring the @ts-ignore comments.

Set Nested Value

Below is a setValue function which takes an object, a path, and a value. The path is a string of dot-separated keys. The function should set the value at the given path. The path parameter will have autocomplete and the value parameter will be required to be of the same type as the property at that path. See the playground for examples. This function works very similarly to lodash.set, but with stronger type safety.

The SchemaForm component for SvelteKit is also using a modified version of this function.

The sort of recursive conditional types with template literal type manipulation being done in Paths<T> and PathValue<T, P> are taxing on the compiler (you can easily get explicit recursion limit warnings, or worse, exponentially long compile times) and have various edge cases.

type Idx<T, K> = K extends keyof T ? T[K] :
  number extends keyof T ? K extends `${number}` ? T[number] : never : never

type Join<K, P> = K extends string | number ?
  P extends string | number ?
  `${K}${"" extends P ? "" : "."}${P}`
  : never : 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, ...0[]]

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] : ""

type PathValue<T, P extends Paths<T, 4>> = P extends `${infer Key}.${infer Rest}`
  ? Rest extends Paths<Idx<T, Key>, 4>
  ? PathValue<Idx<T, Key>, Rest>
  : never
  : Idx<T, P>

function setValue<
  T extends { [key: string]: any },
  P extends Paths<T, 4>
>(obj: T, path: P, value: PathValue<T, P>) {
  if (typeof path === "string") {
    const pList = path.split(".");
    const lastKey = pList.pop();
    const pointer = pList.reduce(
      (accumulator: { [x: string]: any }, currentValue: string | number) => {
        if (accumulator[currentValue] === undefined)
          accumulator[currentValue] = {};
        return accumulator[currentValue];
      },
      obj
    );
    if (typeof lastKey !== "undefined") {
      pointer[lastKey] = value;
      return obj;
    }
  }
  return obj;
}

Update: Resolve Recusive Type Issue

Added 2023-09-15

To resolve the depth issue, you can use the following type to extend PathValue and make it safer and more performant.

type LimitDepth<
  T,
  TLength = 5,
  TDepth extends unknown[] = []
> = TDepth['length'] extends TLength
  ? never
  : T extends (...args: infer Args) => infer R 
  ? (...args: LimitDepth<Args, TLength, [unknown, ...TDepth]>) => LimitDepth<R, TLength, [unknown, ...TDepth]>
  : T extends object
  ? {
      [K in keyof T]: LimitDepth<T[K], TLength, [unknown, ...TDepth]>
    }
  : T extends Array<infer U>
  ? Array<LimitDepth<U, TLength, [unknown, ...TDepth]>>
  : T;

type PathValue<T, P extends Paths<T, 10>, TLength = 10> = P extends `${infer Key}.${infer Rest}`
	? Rest extends Paths<Idx<LimitDepth<T, TLength>, Key>, 10>
		? PathValue<Idx<LimitDepth<T, TLength>, Key>, Rest>
		: never
	: Idx<LimitDepth<T, TLength>, P>;

Prettify

The Prettify type is a simple type which takes an object type with intersections and combines them to make them easier to read.

type Prettify<T> = {
    [K in keyof T]: T[K];
} & {};

Combine Two Object Types

The Combine type is a simple type which takes 2 object types and combines them into one.