TypeScript's Record Type
A Structured Approach to Lookup Objects

Introduction
A couple of weeks ago, I was working on redesigning our notifications UI. Notifications are a primary way applications communicate with end users—whether to reflect application state or to acknowledge a user’s actions. Because of that, their visual clarity and consistency matter more than we often realise.
During this redesign, I was introduced to a TypeScript utility type I didn’t know I needed at the time. This article explores what the Record type is, how it enables strongly typed lookup objects, and where it fits best in real-world applications.
What is a Record Type
Record is a built-in TypeScript utility type used to define object types with a fixed set of keys and a uniform value type. Its general form/syntax is:
Record<K, T>
Where:
Krepresents the allowed keys (typically a union of string or number literals)Trepresents the type of each corresponding value
A Record expresses the idea that for every key in K, there must exist a value of type T.
Unlike index signatures (e.g. { [key: string]: string }), Record is intentionally restrictive. It does not describe “any possible key,” but rather a fixed, known set of keys, making it ideal for predictable mappings.
What are lookup objects?
A lookup object is an object used to retrieve values by key rather than deriving those values through control flow. Rather than evaluating conditions at runtime, lookup objects encode decisions directly into data structures.
For example, UI states, status labels, colour mappings, and configuration values are often better represented as mappings than as executable logic. This approach improves readability and reduces branching complexity while making the relationship between keys and values explicit.
Record formalises this pattern by ensuring that the lookup object is both complete and type-safe. When used together, Record lookup objects provide a declarative alternative to conditional logic. By defining both the valid keys and the expected value type, the compiler can enforce correctness at compile time rather than allowing inconsistencies to surface at runtime.
Practical example: Styling a notification UI by severity (Next.js)
In my case, I was styling a notification component where the visual presentation changed based on severity. The design system was already defined, and the variables were known upfront:
The icon
The border colour
The title colour
Each of these needed to change depending on whether the notification was a success, error, warning, or info message. I found it ideal to utilise a Record-based lookup object.
Defining the notification types
First, define the allowed severity levels. This establishes a closed set of values that the UI can meaningfully respond to.
type ToastType = "success" | "error" | "warning" | "info";
Defining the style contract
Instead of mapping each severity to a single value, we define a structured style object that represents everything the UI needs. This ensures that every severity provides a complete and consistent styling configuration
type NotificationStyle = {
icon: React.ReactNode;
borderColour: string;
titleColour: string;
};
Creating the lookup object with Record
Now we define the lookup object
const notificationStyles: Record<
NotificationSeverity,
NotificationStyle
> = {
success: {
icon: <CheckCircleIcon />,
borderColor: "#16a34a",
titleColor: "#15803d",
},
error: {
icon: <XCircleIcon />,
borderColor: "#dc2626",
titleColor: "#b91c1c",
},
warning: {
icon: <ExclamationTriangleIcon />,
borderColor: "#d97706",
titleColor: "#b45309",
},
info: {
icon: <InformationCircleIcon />,
borderColor: "#2563eb",
titleColor: "#1d4ed8",
},
};
At this point, TypeScript enforces several important constraints:
Every
NotificationSeverityvalue must be present. ✅No additional severities can be introduced accidentally. ✅
Each severity must define all required UI properties. ✅
The shape of the styling object is guaranteed across all cases. ✅
If a new severity is added later (for example, "neutral"), the compiler will immediately surface every place that requires an update.
Consuming the lookup in a Next.js component
With the lookup object in place, the component logic becomes trivial. There is no conditional rendering logic here. No if, no switch, no fallback cases. The component simply declares what it needs and retrieves it directly from the lookup.
type NotificationProps = {
severity: NotificationSeverity;
title: string;
message: string;
};
export function Notification({
severity,
title,
message,
}: NotificationProps) {
const { icon, borderColor, titleColor } = notificationStyles[severity];
return (
<div
style={{ borderColor }}
className="border-l-4 rounded-md p-4"
>
<div className="flex items-start gap-3">
{icon}
<div>
<h4 style={{ color: titleColor }}>{title}</h4>
<p className="text-sm text-gray-600">{message}</p>
</div>
</div>
</div>
);
}
Pros
Type safety: Ensures thorough mappings and prevents undefined access
Improved readability: Replaces conditional logic with declarative data
Refactor-friendly: Adding or removing keys surfaces necessary changes immediately
Clear intent: The mapping between keys and values is explicit and centralised
Cons
Despite its benefits:
It is not well-suited for dynamic or user-defined keys
It can introduce unnecessary rigidity in loosely structured data
For very small or throwaway mappings, it may feel verbose
As with any abstraction, its value depends on context and intent.
Use Cases
Record is most effective when applied to:
UI state-to-message mappings
Status or error code lookups
Configuration objects with fixed options
Permission or role-based access mappings
Internationalization dictionaries
In each case, the domain is finite, and correctness depends on completeness.
Conclusion
The Record utility type is not a replacement for logic, but a tool for eliminating logic where it is unnecessary or predictable. By encoding known relationships directly into types, it reduces ambiguity and shifts error detection earlier in the development process. It represents a move toward more declarative, intentional code, code that communicates constraints clearly and fails early when those constraints are violated.
As I grow more and more technically, I come to appreciate TypeScript and the safety it provides in our codebases. I look forward to discovering more types and to documenting these patterns in my career.
About the author
Full Stack Software Engineer | Developer Advocate | Community Manager | Contant Creator | Technical Writer | Tech Blogger | Open Source Contributor
Comments
Join the discussionNo comments yet. Be the first to comment.
Full Stack 🚀
This section will be focusing more on the functionality side of things with minimal CSS in mind. I will be discussing learnings from various frontend (NextJS, Angular...) and backend (.NET. Node/Express...) frameworks





