Adding a type assertion to an API response does not change its runtime shape. Network data, user input, and local configuration can all differ from what we expect.
One of the most useful roles of a type system is showing us where trust has not yet been established.
Keep unknown data unknown
Use unknown when external data enters the program. It keeps uncertainty visible until validation establishes a reliable internal value.
type Author = { name: string; bio: string }
function parseAuthor(value: unknown): Author {
if (
typeof value !== 'object' ||
value === null ||
!('name' in value) ||
!('bio' in value) ||
typeof value.name !== 'string' ||
typeof value.bio !== 'string'
) {
throw new Error('Invalid author data')
}
return { name: value.name, bio: value.bio }
}The function validates the input and creates a value that follows the internal contract. Downstream code no longer needs to repeat the same defensive checks.
Keep errors near their source
If an article has no title, report the filename and missing field when reading it. That is more useful than waiting for an empty area to appear on the page.
The closer an error is to its source, the cheaper it is to diagnose. A precise message is part of the maintainer’s experience too.
Leave unnecessary possibilities outside
After conversion at the boundary, the internal model can be stricter. Normalize dates to ISO strings and missing tag lists to empty arrays.
The list component can then focus on presentation. It does not need to know whether the data came from a file, database, or API, and it does not need to normalize missing values on every render.
Start with one boundary
There is no need to refactor everything at once. Pick a data entry point that frequently causes trouble, add validation and clear error messages, then see whether the downstream code becomes simpler.
Good type design does not eliminate uncertainty from reality. It gives uncertainty a clear place to live.
Thanks for reading.
There’s always more to explore. See you in the next one.