This is a short note about TypeScript; more of a reference URL to link to.
Say, you have an interface that is used for errors and warnings:
interface NotificationProps {
type: 'error' | 'warning'
message: string
}
Now we get a new requirement: warning notification must have a button "Fix" (the text may vary).
Naive way would be to extend the interface with optional properties:
interface NotificationProps {
type: 'error' | 'warning'
message: string
buttonText?: string
buttonAction?: () => void
}
Less code to change; the interface already exists and is used everywhere; just add a few properties and you're ready to roll, yes? No.
The interface from before had two valid states:
{ type: "error", message }{ type: "warning", message }
By adding two optional properties, we have created 8 possible combinations:
- error + buttonText + buttonAction
- error + no buttonText + buttonAction
- error + buttonText + no buttonAction
- error + no buttonText + no buttonAction
- warning + buttonText + buttonAction
- warning + no buttonText + buttonAction
- warning + buttonText + no buttonAction
- warning + no buttonText + no buttonAction
Every optional property doubles the states that are representable.
Most of these states are invalid in the light of the requirements.
It's quite possible that the code you have right now doesn't get into these invalid states! But that's not a guarantee that such things won't happen in the future. We keep the restriction of what states are valid and not in the head of the developer; TypeScript can't help us. That's not very reliable in the long run.
Better solution
Better path, from maintenance point of view, is to use unions to describe those two particular valid states:
type NotificationProps = { type: 'error', message: string } | {
type: 'warning',
message: string,
buttonText: string,
buttonAction: () => void
}
You can even factor out the common part, but that seems like overkill for now:
type NotificationProps = { message: string } & (
| { type: 'error' }
| { type: 'warning', buttonText: string, buttonAction: () => void }
)
TS Playground to play with.
This article was originally written in Russian.