DEV Community

Alex
Alex

Posted on

TS function overloading - 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,
)
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode

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,
) {
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.

Result

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.

Top comments (0)