Streamline Your Typescript Projects with These Useful Abstract Utils and Helpers
Tags: typescript, abstractutils, webdev, reactkit
Published: 2023-09-09 10:34:00

✨ [**Watch on YouTube**](https://youtu.be/bWLeNhFaGRg)  |  🐙 [GitHub](https://github.com/radzionc/reactkit/tree/main/utils)

In this post, I aim to share some of the most useful abstract utilities and helper functions that I use across different TypeScript projects. I believe these will save you time and help you to think about your code in a more abstract way. You can find all these functions in the `utils` package under the [ReactKit repository](https://github.com/radzionc/reactkit).

### match: A functional switch statement

The first function I want to introduce is `match`. It's a more refined version of the `switch-case` statement.

```ts
export function match<T extends string | number | symbol, V>(
  value: T,
  handlers: { [key in T]: () => V }
): V {
  const handler = handlers[value]

  return handler()
}
```

Here's an example demonstrating how I use it to construct variants of the `Button` component based off of the `kind` prop:

```tsx
  ${({ kind }) =>
    match(kind, {
      primary: () => css`
        background: ${getColor('primary')};
        color: ${getColor('white')};
      `,
      secondary: () => css`
        background: ${getColor('mist')};
        color: ${getColor('contrast')};
      `,
      reversed: () => css`
        background: ${getColor('contrast')};
        color: ${getColor('background')};
      `,
      alert: () => css`
        background: ${getColor('alert')};
        color: ${getColor('white')};
      `,
      outlined: () => css`
        border: 1px solid ${getColor('mistExtra')};
        color: ${getColor('contrast')};
      `,
      outlinedAlert: () => css`
        border: 1px solid ${getColor('alert')};
        color: ${getColor('alert')};
      `,
      ghost: () => css`
        color: ${getColor('contrast')};
      `,
      ghostSecondary: () => css`
        color: ${getColor('textSupporting')};
      `,
    })}
```

The `match` function takes two arguments: a value to match against, and an object mapping each possible value to a function. The function that corresponds to the value is then called, and its result is returned. We typically use `match` for union types, and it will flag a TypeScript error if you forget to provide a resolver for one of the possible values.

### shouldBeDefined: throw an error if value is undefined

The next function, `shouldBeDefined`, is designed to throw an error if the value is undefined. Sometimes we find ourselves in situations where we know a value is defined, but the type could be undefined. In these scenarios, we don't need to handle undefined values explicitly but simply need to throw an error if the value is missing.

```tsx
export function shouldBeDefined<T>(
  value: T | undefined,
  valueName: string = "value"
): T {
  if (value === undefined) {
    throw new Error(`${valueName} is undefined`)
  }

  return value
}
```

One example is accessing environment variables. Their type is `string | undefined`, but we know that they were established before launching our application.

```tsx
const apiUrl = shouldBeDefined(process.env.NEXT_PUBLIC_API_URL)
```

### getRecord: convert array to an object

The third function, `getRecord`, is used to convert an array into an object.

```tsx
export function getRecord<T>(
  items: T[],
  getId: (item: T) => string
): Record<string, T> {
  const record: Record<string, T> = {}

  items.forEach((item) => {
    record[getId(item)] = item
  })

  return record
}
```

At [Increaser](https://increaser.org), I use it to convert a list of projects into a record where the key is the project id. This allows for easier access to project-related information by id, compared to searching using the `find` method on an array.

```ts
const projectsRecord = getRecord(projects, (project) => project.id)
```

### memoize: cache function results

The fourth function, `memoize`, caches the result of a function based on its arguments. If the same function is called with the same argument a second time, it will return the previous result without re-evaluating the function.

```tsx
export const memoize = <T extends (...args: any[]) => any>(
  func: T,
  getKey?: (...args: any[]) => string
): T => {
  const cache = new Map<string, ReturnType<T>>()

  const memoizedFunc = (...args: Parameters<T>) => {
    const key = getKey ? getKey(...args) : JSON.stringify(args)

    const cachedResult = cache.get(key)

    if (!cachedResult) {
      const result = func(...args)
      cache.set(key, result)

      return result
    }

    return cachedResult
  }

  return memoizedFunc as T
}
```

Usually, I use this to avoid initializing the same server twice or repeating requests for data that hasn't changed. While this implementation is quite simple, it can be enhanced by adding a cache expiration time, for instance.

### toPercents: convert number to percents

The fifth function, `toPercents`, converts a number to a percentage.

```tsx
type Format = "round"

export const toPercents = (value: number, format?: Format) => {
  const number = value * 100

  return `${format === "round" ? Math.round(number) : number}%`
}
```

This function is extremely useful in two scenarios: One, to display a ratio rounded to the nearest percent to users. Second, it helps when converting a number to a percentage value for CSS properties, especially for absolutely-positioned elements.

### splitBy: split array into chunks

An interesting function in the `array` directory is `splitBy`. It divides an array into chunks.

```tsx
export function splitBy<T>(items: T[], organize: (item: T) => 0 | 1) {
  const result: [T[], T[]] = [[], []]

  items.forEach((item) => {
    const bucket = result[organize(item)]
    bucket.push(item)
  })

  return result
}
```

Here's an example of it being used to divide color options into 'used' and 'free' sections in the color picker component.

```tsx
const [free, used] = splitBy(colors, (value) => (usedValues.has(value) ? 1 : 0))
```

### withoutDuplicates: remove duplicates from an array

Another useful array function is `withoutDuplicates`, which takes an array and a function to compare two items.

```ts
export function withoutDuplicates<T>(
  items: T[],
  areEqual: (a: T, b: T) => boolean = (a, b) => a === b
): T[] {
  const result: T[] = []

  items.forEach((item) => {
    if (!result.find((i) => areEqual(i, item))) {
      result.push(item)
    }
  })

  return result
}
```

### range: create an array of a given length

The last array function I want to discuss is `range`; it produces an array of a specified length.

```ts
export const range = (length: number) => Array.from({ length }, (_, i) => i)
```

This is useful for instances like the bar chart component that illustrates how much you worked on a specific day. Here, we make an array with a length of 7 and populate it with data for the bar chart.

```tsx
result = range(D_IN_WEEK).map((index) => {
  const dayStartsAt = weekStartedAt + MS_IN_DAY * index
  const dayEndsAt = dayStartsAt + MS_IN_DAY
  const value =
    getSetsSum(
      currentWeekSets.filter(
        (set) => set.end < dayEndsAt && set.start > dayStartsAt
      )
    ) / MS_IN_SEC

  return {
    isCurrent: index === currentWeekday,
    value,
    label: getShortWeekday(index),
  }
})
```

### convertDuration: convert between time units

A major part of my work with Increaser involves numerous time-tracking features, so I've organized all the time-related functions in the `time` directory. An interesting function here is `convertDuration`, which converts between different time units.

```ts
import { MS_IN_DAY, MS_IN_HOUR, MS_IN_MIN, MS_IN_SEC } from "."

export type DurationUnit = "ms" | "s" | "min" | "h" | "d"

const msInUnit: Record<DurationUnit, number> = {
  ms: 1,
  s: MS_IN_SEC,
  min: MS_IN_MIN,
  h: MS_IN_HOUR,
  d: MS_IN_DAY,
}

export const convertDuration = (
  value: number,
  from: DurationUnit,
  to: DurationUnit
) => {
  const result = value * (msInUnit[from] / msInUnit[to])

  return result
}
```

For instance, I may have a value expressed in minutes, but I want to display it in hours. By using `convertDuration`, I can pass the value, `min` as the `from` unit, and `h` as the `to` unit.

```tsx
<Text weight="bold">
  {Math.round(convertDuration(workBudgetInMin, "min", "h"))}h / week
</Text>
```

### inTimeZone: convert date to a different timezone

The last time-related function I want to share is `inTimeZone`, which adapts a time as if it was in a different timezone.

```ts
import { getCurrentTimezoneOffset } from "./getCurrentTimezoneOffset"
import { convertDuration } from "./convertDuration"

export const inTimeZone = (timestamp: number, targetTimeZoneOffset: number) => {
  const offsetDiff = targetTimeZoneOffset - getCurrentTimezoneOffset()
  return timestamp + convertDuration(offsetDiff, "min", "ms")
}
```

At [Increaser](https://increaser.org), I manage a scoreboard of the most productive users of the month. However, these users are located in various time zones, which means their month starts at different times. On the backend, I gain the start of the current month in the server's timezone and then use `inTimeZone` to get the start of the month in the user's timezone. This allows me to filter their sessions by the current month.

```ts
const now = Date.now()
const monthStartedAt = inTimeZone(getMonthStartedAt(now), timeZone)
const monthStartedAt = inTimeZone(getMonthStartedAt(now), timeZone)
const currentMonthSets = sets.filter((set) => set.start > monthStartedAt)
```

There are many more handy functions available at [ReactKit](https://github.com/radzionc/reactkit), but this should suffice for now. In upcoming posts, I will delve into essential React components and hooks.