1. Defining Object Types: Inline, Type, and Interface#
There are three ways to define an object’s shape in TypeScript:
1. Inline Object Types#
Useful for small, one-off function parameters, but hard to reuse across a codebase.
function printUser(user: { name: string; age: number }) {
console.log(`${user.name} is ${user.age} years old.`);
}2. Type Aliases (type)#
Allows saving an object shape under an alias name. Supports primitives, unions, and tuples.
type User = {
name: string;
age: number;
};3. Interfaces (interface)#
Designed specifically for defining object contracts and class interfaces. Supports interface merging and inheritance (extends).
interface UserInterface {
name: string;
age: number;
}2. Structural Typing (Duck Typing)#
TypeScript utilizes a Structural Type System (often described as static “duck typing”).
In nominal languages like Java or C#, a class User is only compatible with User if it explicitly declares that relationship. In TypeScript, compatibility is determined solely by shape.
If two objects share the required properties with compatible types, TypeScript considers them the same type, regardless of how or where they were defined.
interface Point2D {
x: number;
y: number;
}
interface Vector2D {
x: number;
y: number;
}
function logCoordinates(point: Point2D) {
console.log(`X: ${point.x}, Y: ${point.y}`);
}
const myVector: Vector2D = { x: 10, y: 20 };
// 🟢 WORKED! Vector2D has the exact same structure as Point2D!
logCoordinates(myVector);
Extra Properties in Structural Typing#
If an object has more properties than required by the target type, TypeScript still permits it when assigned via an intermediary variable:
const point3D = { x: 1, y: 2, z: 3 };
// Allowed! point3D satisfies Point2D because it has 'x' and 'y' of type number.
logCoordinates(point3D);
3. Excess Property Checking#
While structural typing allows objects with extra properties, TypeScript applies a special rule called Excess Property Checking when an object literal is passed directly inline.
// ❌ Compiler Error: Object literal may only specify known properties,
// and 'z' does not exist in type 'Point2D'.
// logCoordinates({ x: 1, y: 2, z: 3 });
Why Excess Property Checks Exist#
Passing { x: 1, y: 2, z: 3 } directly inline is almost always a developer typo. If you pass an inline object literal, TypeScript assumes you intended to match the type exactly, and flags any extraneous keys as errors.
To bypass excess property checks, assign the object literal to a temporary variable first:
const payload = { x: 1, y: 2, z: 3 };
logCoordinates(payload); // Allowed! (Triggers structural check, not excess check)
4. Optional Properties vs undefined#
You mark a property as optional by placing a question mark (?) before the colon:
interface UserProfile {
id: string;
email: string;
bio?: string; // Optional property
}The Critical Difference: bio?: string vs bio: string | undefined#
| Declaration | Can Omit Property Key? | Valid Values |
|---|---|---|
bio?: string | YES | "hello", undefined, or omit { id: "1", email: "[email protected]" } |
bio: string | undefined | NO | "hello", undefined (Key MUST be explicitly present!) |
// Valid for bio?: string
const user1: UserProfile = { id: "1", email: "[email protected]" };
type StrictBio = {
bio: string | undefined;
};
// ❌ Error: Property 'bio' is missing in type '{ ... }' but required in type 'StrictBio'.
// const user2: StrictBio = {};
const user2: StrictBio = { bio: undefined }; // Valid!
5. readonly Properties & Immutability#
The readonly modifier prevents property reassignment after object initialization.
interface Account {
readonly accountId: string;
balance: number;
}
const account: Account = {
accountId: "acc_99812",
balance: 5000,
};
account.balance = 5500; // Valid!
// ❌ Compiler Error: Cannot assign to 'accountId' because it is a read-only property.
// account.accountId = "acc_00000";
readonly is compile-time enforcement only. It does not prevent mutation at runtime in compiled JavaScript unless combined with Object.freeze().
6. Index Signatures#
When an object can contain arbitrary keys (like a dictionary or lookup map), use an Index Signature:
interface FeatureFlags {
// Allows any string key mapped to a boolean value
[flagName: string]: boolean;
}
const flags: FeatureFlags = {
enableNewUI: true,
enableBetaCheckout: false,
darkTheme: true,
};
const isBetaEnabled = flags["enableBetaCheckout"]; // Inferred Type: boolean
You can also combine fixed properties with index signatures, provided the fixed properties match the index signature return type:
interface ConfigMap {
env: string; // Fixed property
[key: string]: string; // Index signature matching string values
}7. Extending Interfaces (extends)#
Interfaces can inherit properties from other interfaces using the extends keyword:
interface Entity {
id: string;
createdAt: Date;
}
interface UserEntity extends Entity {
name: string;
email: string;
}
const user: UserEntity = {
id: "usr_100",
createdAt: new Date(),
name: "John Doe",
email: "[email protected]",
};Summary & Next Steps#
In this episode:
- We learned the 3 ways to define object types: inline,
type, andinterface. - We unpacked Structural Typing (matching shapes rather than nominal type names).
- We analyzed Excess Property Checks on direct inline object literals.
- We compared
key?: string(omittable) vskey: string | undefined(required key). - We introduced
readonlymodifiers, index signatures, andinterface extends.
In Episode 5: Arrays and Tuples, we will explore ordered data collections, typed arrays, fixed-length tuples, and readonly arrays!

