Question
TypeScript Dictionary Initialization and Structural Type Checking Explained
Question
Given the following TypeScript code:
interface IPerson {
firstName: string;
lastName: string;
}
const persons: { [id: string]: IPerson } = {
p1: { firstName: "F1", lastName: "L1" },
p2: { firstName: "F2" }
};
Why is this initialization sometimes not rejected by TypeScript, even though the p2 object does not include the required lastName property from IPerson?
Short Answer
By the end of this page, you will understand how TypeScript checks object shapes inside dictionary-like objects, why missing properties may appear to slip through in some situations, and how index signatures, structural typing, and compiler settings affect type safety.
Concept
TypeScript uses structural typing, which means it checks whether a value has the required shape rather than whether it was created from a specific class or type name.
In this example, the dictionary type is:
{ [id: string]: IPerson }
This means:
- keys are strings
- every value must match
IPerson
And IPerson requires both:
firstName: stringlastName: string
So at first glance, this object should be rejected:
p2: { firstName: "F2" }
because lastName is missing.
In modern TypeScript, this should produce an error. If it does not, the usual reasons are:
- you are using an older TypeScript version
- the code is being checked less strictly by tooling
- the object passed through a type like
any - the environment is not actually type-checking this file as TypeScript
The core concept is that index signatures do not weaken the required structure of the values. If a dictionary says each value is an , every entry must satisfy .
Mental Model
Think of a dictionary type like a set of labeled lockers.
- The locker numbers are the keys, such as
p1andp2. - The rule says every locker must contain a complete person form.
- A complete person form requires both a first name and a last name.
If one locker contains a form with only a first name, it should fail inspection.
So the important idea is: the key can vary, but the value shape cannot.
The locker label is flexible. The contents are not.
Syntax and Examples
A dictionary in TypeScript is often written with an index signature:
interface IPerson {
firstName: string;
lastName: string;
}
const persons: { [id: string]: IPerson } = {
p1: { firstName: "Ada", lastName: "Lovelace" },
p2: { firstName: "Alan", lastName: "Turing" }
};
This is valid because each value matches IPerson.
You can also write the same idea using Record:
const persons: Record<string, IPerson> = {
p1: { firstName: "Ada", lastName: "Lovelace" },
p2: { firstName: "Alan", lastName: }
};
Step by Step Execution
Consider this example:
interface IPerson {
firstName: string;
lastName: string;
}
const persons: { [id: string]: IPerson } = {
p1: { firstName: "F1", lastName: "L1" },
p2: { firstName: "F2" }
};
Here is what TypeScript checks step by step:
-
It sees that
personsmust be an object whose string keys map toIPersonvalues. -
It looks at key
p1. -
The value is:
{ firstName: "F1", lastName: "L1" } -
That object has both required properties, so
p1is valid. -
It then looks at key .
Real World Use Cases
Dictionary types appear often in real projects.
API response maps
type UsersById = Record<string, IPerson>;
Used when an API returns users indexed by ID.
In-memory caches
const cache: Record<string, IPerson> = {};
A server may store already-fetched users in a lookup object.
State management
Frontend apps often normalize data by ID:
interface State {
persons: Record<string, IPerson>;
}
This makes updates faster than searching arrays.
Configuration objects
A build tool or app may map names to structured settings:
interface ConfigItem {
enabled: boolean;
path: ;
}
: <, > = {
: { : , : }
};
Real Codebase Usage
In real codebases, developers usually combine dictionary types with a few common patterns.
Validation at boundaries
Data from APIs is often unknown at runtime, even if TypeScript types say otherwise.
function savePerson(person: IPerson) {
// safe to use person.lastName here
}
But before that, many teams validate incoming JSON.
Named aliases for readability
type PersonMap = Record<string, IPerson>;
This is easier to reuse than repeating { [id: string]: IPerson } everywhere.
Optional fields when data is genuinely partial
interface DraftPerson {
firstName: string;
lastName?: string;
}
Teams often separate DraftPerson from IPerson so incomplete data is modeled honestly.
Guard clauses
Common Mistakes
1. Assuming index signatures make nested values loose
Broken assumption:
interface IPerson {
firstName: string;
lastName: string;
}
const persons: { [id: string]: IPerson } = {
p2: { firstName: "F2" }
};
The index signature only makes the keys flexible. The values still must be full IPerson objects.
2. Using any and losing type safety
const data: any = {
p2: { firstName: "F2" }
};
const persons: Record<string, IPerson> = data;
This may compile, but any turns off useful checks.
Avoid it: prefer unknown and validate before assigning.
Comparisons
| Concept | What it means | Good for | Risk |
|---|---|---|---|
{ [id: string]: IPerson } | Object with string keys and IPerson values | Lookup objects | Repeating inline type can be verbose |
Record<string, IPerson> | Utility type version of the same idea | Cleaner syntax | Same behavior, just different syntax |
IPerson[] | Array of people | Ordered lists | Lookup by ID is slower |
Partial<IPerson> | All IPerson fields become optional | Drafts, patches, forms | Too loose if used where full data is required |
Cheat Sheet
interface IPerson {
firstName: string;
lastName: string;
}
const persons: Record<string, IPerson> = {
p1: { firstName: "A", lastName: "B" }
};
Rules
Record<string, IPerson>means every string key maps to anIPerson- every
IPersonvalue must include all required properties - index signatures allow flexible keys, not flexible value shapes
- optional properties must be marked with
? anycan hide errorsas SomeTypedoes not validate runtime data
Equivalent syntax
{ [id: string]: IPerson }
Record<string, IPerson>
Optional field syntax
FAQ
Why should TypeScript reject an object missing a required property?
Because the value does not match the declared shape. If lastName is required in IPerson, every IPerson value must have it.
Is { [id: string]: IPerson } the same as Record<string, IPerson>?
Yes, for this use case they mean the same thing. Record is usually shorter and easier to read.
Why might this code compile in my project?
Possible reasons include an older TypeScript version, incorrect tooling setup, the use of any, or type assertions that silence checking.
Does as IPerson verify the object at runtime?
No. A type assertion only tells TypeScript what to assume. It does not validate the actual data.
How do I allow missing lastName values?
Mark the property as optional:
lastName?: string;
Should I use a dictionary or an array for people?
Use a dictionary when you usually access people by ID. Use an array when order matters or you mostly loop through all items.
What is structural typing in TypeScript?
Mini Project
Description
Build a simple employee directory stored as a dictionary object. Each employee is looked up by ID, and each value must match a required TypeScript interface. This demonstrates how TypeScript checks nested object values inside a dictionary and how optional fields should be modeled intentionally.
Goal
Create a typed employee lookup object where each employee entry satisfies the required interface, then add a safe function to print employee names by ID.
Requirements
- Define an
IEmployeeinterface with requiredfirstName,lastName, androleproperties. - Create a dictionary of employees keyed by employee ID.
- Add at least two valid employee entries.
- Write a function that accepts the dictionary and an ID, then returns a formatted employee name.
- Ensure the code would fail type checking if an employee is missing a required property.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.
Angular @ViewChild Returning Undefined: Lifecycle, Child Components, and Fixes
Learn why Angular @ViewChild can be undefined, when it becomes available, and how to access child components correctly using lifecycle hooks.