DEV Community

Stefano Magni
Stefano Magni

Posted on

RouteManager UI coding patterns: TypeScript

This is an non-exhaustive list of the coding patterns the WorkWave RouteManager's front-end team follows. The patterns are based on years of experience writing, debugging, and refactoring front-end applications with React and TypeScript but evolves constantly. Most of the possible improvements and the code smells are detected during the code reviews and the pair programming sessions.

(last update: 2021, July)

Always use Type-Only Imports and Exports

You must import types through import type and re-export types through export type.

// ❌ don't
import { Categories } from .'/types'
export { Props as ButtonProps } from .'/types'

// ✅ do
import type { Categories } from .'/types'
export type { Props as ButtonProps } from .'/types'
Enter fullscreen mode Exit fullscreen mode

Prefer @ts-expect-error over @ts-ignore

@ts-ignores are banned through an ESLint rule, @ts-expect-error offers a greater experience and scales better in case you are working around a TypeScript limitations or a type inconsistency. Please. always leave a comment to explain why you are using @ts-expect-error.

// ❌ don't
// @ts-ignore
delete mapSelectionAtom.context.category

// ✅ do
// @ts-expect-error category is not valid on `none` but we need to ensure is removed from the object
delete mapSelectionAtom.context.category
Enter fullscreen mode Exit fullscreen mode

Take advantage of Types Inference as much as possible

Write JavaScript the most natural way, don't add types where TypeScript can infer them. Types reduce readability and increase the number of imports.

// ❌ don't
const a:number = 1
const b = useState<number>(1)
const c = [1, 2, 3].map<number>(item => item*2)

// ✅ do
const a = 1
const b = useState(1)
const c = [1, 2, 3].map(item => item*2)


// ❌ don't
function isEven(a: number): boolean {
  const result:boolean = a % 2
  return result
}

// ✅ do
function isEven(a: number) {
  return a % 2 === 0
}
Enter fullscreen mode Exit fullscreen mode

Generics

Every case is different and must be treated separately but, as a rule of thumb, avoid Generics. If you must use them, please avoid using T, K, etc. but prefer explicit names.

// ❌ don't
export type UseFormFieldOptions<
  T extends Record<string, FormField>,
  K extends FormError<keyof T>
> = {
  // ...
}

// ✅ do
export type UseFormFieldOptions<
  FIELDS extends Record<string, FormField>,
  ERROR extends FormError<keyof FIELDS>
> = {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Avoid TypeScript guards

TypeScript guards are useful when the data comes from external, untyped, sources. Internal, controlled, code must prefer simpler ways to help generic functions.

type Tag = { id: string }
type Info = { uid: string }

type GpsEntity = Tag | Info

// ❌ don't
export function getGpsEntityId<T extends GpsEntity>(entity: T) {
  if (isTag(entity)) {
    return entity.id
  }
  if (isInfo(entity)) {
    return entity.uid
  }
}

// ✅ do
export function getGpsEntityId(entity: T, type: 'tag' | 'info') {
  switch (type) {
    case 'tag':
      return ((entity as any) as Tag).id
    case 'info':
      return ((entity as any) as Info).uid
  }
}
Enter fullscreen mode Exit fullscreen mode

Use Array.reduce's Generic

Always take advantage of the Array.reduce's Generic type instead of typing the parameters passed to it.

// ❌ don't
[1, 2, 3].reduce((acc: number, item) => {
  return acc + item
}, 0)

// ✅ do
[1, 2, 3].reduce<number>((acc, item) => {
  return acc + item
}, 0)
Enter fullscreen mode Exit fullscreen mode

Prefer discriminated unions over optional keys

Optional keys save you from runtime errors but aren't expressive, Discriminated Unions are.

// ❌ don't
type Order = {
  status: 'ready' | 'inProgress' | 'complete'
  name: string
  at?: Location
  expectedDelivery?: Date
  deliveredAt?: Date
  proofOfDeliveries?: Proof[]
}

function getEmailMessage(order: Order) {
  if(order.at) {
    return `${order.name} is at ${order.at}`
  }
  if(order.expectedDelivery) {
    return `${order.name} will be delivered ${order.expectedDelivery}`
  }
  if(order.deliveredAt) {
    return `${order.name} has been delivered at ${order.deliveredAt}`
  }
}

// ✅ do
type Order = {
  name: string
} & ({
  status: 'ready'
  at: Location
} | {
  status: 'inProgress'
  expectedDelivery: Date
} | {
  status: 'complete'
  deliveredAt: Date
  proofOfDeliveries: Proof[]
})

function getEmailMessage(order: Order) {
  switch(order.status) {
    case 'ready':
      return `${order.name} is at ${order.at}`

    case 'inProgress':
      return `${order.name} will be delivered ${order.expectedDelivery}`

    case 'complete':
      return `${order.name} has been delivered at ${order.deliveredAt}`

  }
}
Enter fullscreen mode Exit fullscreen mode

Discussion (0)