Skip to main content

TypeScript function overloading: a real-world example

Let’s dive into less frequent Typescript feature - function overloading with a realistic example.

Intro

Have a custom hook

export function useUrlState<T extends JSONCompatible>(
  defaultState: T,
  searchParams?: object,
);

At some moment I need to add more arguments to it, possibly more in the future. Hard to remember what Nth argument is, and calling a function like useUrlState(firstArg, null, null, fourthArg) is ridiculous. It will be way easier to pass arguments inside an object like this:

export function useUrlState<T extends JSONCompatible>({
  defaultState,
  searchParams,
  replace,
}: {
  defaultState: T;
  searchParams?: object;
  replace?: boolean;
});

I will convert the function to a new format and keep it backward compatible with the existing implementation.

Implementation

First, need to add overload signatures right above function implementation. Overload signatures are all possible ways a function can be called, with different argument’s type and quantity.


/**
 * @deprecated Pass arguments in a object `useUrlState({ defaultState: form, searchParams })`
 *
 *  * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/next/useUrlState#api}
 */
export function useUrlState<T extends JSONCompatible>(defaultState: T, searchParams?: object): {
  state: DeepReadonly<T>,
  updateState: (value: Partial<DeepReadonly<T>>,
  updateUrl: (value?: Partial<DeepReadonly<T>>) => void,
  getState: () => DeepReadonly<T>
}
/**
 * NextJS hook. Returns `state`, `updateState`, and `updateUrl` functions
 *
 * @param {JSONCompatible<T>} [defaultState] Fallback (default) values for state
 * @param {?SearchParams<T>} [searchParams] searchParams from Next server component
 */
export function useUrlState<T extends JSONCompatible>({ defaultState, searchParams }: {
  defaultState: T, searchParams?: object, replace?: boolean
}): {
  state: DeepReadonly<T>,
  updateState: (value: Partial<DeepReadonly<T>>) => void,
  updateUrl: (value?: Partial<DeepReadonly<T>>) => void,
  getState: () => DeepReadonly<T>
} // <- notice that should implicitly define returned value
// implementation
export function useUrlState<T extends JSONCompatible>(
  defaultState: T | { defaultState: T, searchParams?: object, replace?: boolean },
  searchParams?: object,
) {

Tricky part is that signatures should be compatible with implementation, so have this defaultState: T | { defaultState: T, searchParams?: object, replace?: boolean }

I assume that if the first argument has a specific key, it is a new object format.

const _defaultState = (
  "defaultState" in defaultState ? defaultState.defaultState : defaultState
) as T;
const _searchParams = (
  "defaultState" in defaultState ? defaultState.searchParams : searchParams
) as object | undefined;
const _replace = (
  "defaultState" in defaultState ? (defaultState.replace ?? true) : false
) as boolean;

Also, can notice that replace argument has default value true for a new format, but for old one it’s false.

Let’s see how it works.

Notice that we have different JSDoc comments for each signature, old one marked with @deprecated tag.

Official docs https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads

Tnx for reading :)

Leave a comment about your experience, or if you have ideas how to do it more elegantly.

Share

Also published on dev.to .

Comments

Sign in with GitHub to leave one. Threads are public GitHub Discussions — you can read and reply from there instead.