31
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)
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'
@ts-ignore
s 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
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
}
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>
> = {
// ...
}
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
}
}
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)
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}`
}
}