Objects
Objects are a fundamental building block in TypeScript. They enable you to represent and handle complex data structures with the added benefit of static typing. In this module, you’ll learn how to create and manipulate objects in TypeScript, including defining types, adding, removing, and modifying values. Additionally, you’ll understand how to use objects in conjunction with functions and arrays while maintaining type safety.
What is an object in TypeScript
In essence, objects are hash tables with unique keys and their corresponding values. TypeScript adds static typing to JavaScript objects, allowing you to define the shape of your objects and catch errors at compile time.
Objects can be created using:
new Object()(not recommended){}(recommended)- Type annotations and interfaces
Objects inherit methods like toString() because of their prototype:
let o1: object = new Object();
console.log((o1 as any).toString()); // [object Object]
let o2: object = {};
console.log((o2 as any).toString()); // [object Object]
Object Type Annotations
TypeScript allows you to define the shape of objects using type annotations:
// Inline type annotation
let person: { name: string; age: number } = {
name: "Alice",
age: 30
};
// Type alias
type Person = {
name: string;
age: number;
email?: string; // Optional property
};
let person1: Person = {
name: "Bob",
age: 25
};
let person2: Person = {
name: "Charlie",
age: 35,
email: "charlie@example.com"
};
Interfaces
Interfaces provide a powerful way to define object types:
interface User {
name: string;
birthYear: number;
estimatedAge(): number;
toString(): void;
}
let user: User = {
name: "Mathias",
birthYear: 2006,
estimatedAge: function(): number {
return new Date().getFullYear() - this.birthYear;
},
toString: function(): void {
console.log(`My name is ${this.name} and I am ${this.estimatedAge()} years old`);
}
};
user.toString(); // My name is Mathias and I am 19 years old
Bracket vs Dot Notation
TypeScript supports both bracket and dot notation for accessing properties:
Bracket Notation
Useful for dynamic property access:
interface Person {
[key: string]: any; // Index signature for dynamic properties
}
let p1: Person = {};
p1["name"] = "Mathias";
p1["birthYear"] = 2006;
p1["estimatedAge"] = function(): number {
return new Date().getFullYear() - this["birthYear"];
};
p1["toString"] = function(): void {
console.log(`My name is ${this["name"]} and I am ${this["estimatedAge"]()} years old`);
};
p1["toString"](); // My name is Mathias and I am 19 years old
Dot Notation
The preferred way when property names are known at compile time:
interface Person {
name: string;
birthYear: number;
estimatedAge(): number;
toString(): void;
}
let p2: Person = {
name: "Mikkel",
birthYear: 2003,
estimatedAge: function(): number {
return new Date().getFullYear() - this.birthYear;
},
toString: function(): void {
console.log(`My name is ${this.name} and I am ${this.estimatedAge()} years old`);
}
};
p2.toString(); // My name is Mikkel and I am 22 years old
Object Literals with Type Safety
TypeScript ensures type safety when creating object literals:
interface Person {
name: string;
birthYear: number;
estimatedAge(): number;
toString(): void;
}
let p3: Person = {
name: "Michell",
birthYear: 1966,
estimatedAge(): number {
return new Date().getFullYear() - this.birthYear;
},
toString(): void {
console.log(`My name is ${this.name} and I am ${this.estimatedAge()} years old`);
}
};
p3.toString(); // My name is Michell and I am 59 years old
Readonly and Optional Properties
TypeScript adds modifiers for properties:
interface Product {
readonly id: string; // Cannot be modified after creation
name: string;
price: number;
description?: string; // Optional property
discount?: number;
}
let product: Product = {
id: "123",
name: "Laptop",
price: 999
};
// product.id = "456"; // Error: Cannot assign to 'id' because it is a read-only property
product.price = 899; // OK
product.discount = 10; // OK - optional property can be added
Adding and Removing Properties
You can dynamically add or remove properties, but TypeScript requires proper typing:
interface Person {
name: string;
birthYear: number;
estimatedAge(): number;
toString(): void;
yearsTo18?(): number; // Optional method
}
let p5: Person = {
name: "Villads",
birthYear: 2017,
estimatedAge(): number {
return new Date().getFullYear() - this.birthYear;
},
toString(): void {
console.log(`My name is ${this.name} and I am ${this.estimatedAge()} years old`);
}
};
p5.toString(); // My name is Villads and I am 8 years old
// Add optional method
p5.yearsTo18 = function(): number {
return 18 - this.estimatedAge();
};
console.log(p5.yearsTo18()); // 10
// Remove property
delete (p5 as any).name; // Type assertion needed for delete
p5.toString(); // My name is undefined and I am 8 years old
Iterating Over Object Keys
TypeScript provides type-safe ways to iterate over object properties:
interface Person {
name: string;
birthYear: number;
estimatedAge(): number;
toString(): void;
}
let person: Person = {
name: "Villads",
birthYear: 2017,
estimatedAge(): number {
return new Date().getFullYear() - this.birthYear;
},
toString(): void {
console.log(`My name is ${this.name} and I am ${this.estimatedAge()} years old`);
}
};
// Iterate with for...in
for (const key in person) {
console.log(`${key}: ${typeof person[key as keyof Person]}`);
}
/*
name: string
birthYear: number
estimatedAge: function
toString: function
*/
// Using Object.keys()
Object.keys(person).forEach(key => {
const value = person[key as keyof Person];
if (typeof value === "function") {
value.call(person);
} else {
console.log(`${key}: ${value}`);
}
});
Arrays of Objects
Objects can be elements within typed arrays:
interface Person {
name: string;
birthYear: number;
estimatedAge(): number;
toString(): void;
}
// Shared methods
const estimatedAge = function(this: Person): number {
return new Date().getFullYear() - this.birthYear;
};
const toString = function(this: Person): void {
console.log(`My name is ${this.name} and I am ${this.estimatedAge()} years old`);
};
let people: Person[] = [
{
name: "Michell",
birthYear: 1966,
estimatedAge: estimatedAge,
toString: toString
},
{
name: "Lene",
birthYear: 1964,
estimatedAge: estimatedAge,
toString: toString
}
];
people.push({
name: "Mikkel",
birthYear: 2003,
estimatedAge: estimatedAge,
toString: toString
});
console.log(people.length); // 3
// Iterate with for loop
for (let i = 0; i < people.length; i++) {
people[i].toString();
}
// Iterate with for...of
for (const person of people) {
person.toString();
}
/*
My name is Michell and I am 59 years old
My name is Lene and I am 61 years old
My name is Mikkel and I am 22 years old
*/
JSON and Type Safety
TypeScript provides type-safe JSON serialization:
interface PersonData {
name: string;
birthYear: number;
}
interface Person extends PersonData {
estimatedAge(): number;
toString(): void;
}
// Create array with methods
const estimatedAge = function(this: Person): number {
return new Date().getFullYear() - this.birthYear;
};
const toString = function(this: Person): void {
console.log(`My name is ${this.name} and I am ${this.estimatedAge()} years old`);
};
let people: Person[] = [
{
name: "Michell",
birthYear: 1966,
estimatedAge: estimatedAge,
toString: toString
},
{
name: "Lene",
birthYear: 1964,
estimatedAge: estimatedAge,
toString: toString
}
];
// Serialize to JSON (methods are excluded)
let json: string = JSON.stringify(people);
console.log(json);
// [{"name":"Michell","birthYear":1966},{"name":"Lene","birthYear":1964}]
// Deserialize from JSON
let peopleData: PersonData[] = JSON.parse(json);
console.log(peopleData.length); // 2
// Reconstruct with methods
let reconstructedPeople: Person[] = peopleData.map(data => ({
...data,
estimatedAge: estimatedAge,
toString: toString
}));
reconstructedPeople[0].toString(); // My name is Michell and I am 59 years old
JSON.stringify() and functions
Note that JSON.stringify() does not include functions when serializing objects. Only data properties (strings, numbers, booleans, arrays, objects) are included in the JSON output.
In TypeScript, you can separate data interfaces from full object interfaces to make this distinction clear:
Destructuring with Types
Destructuring in TypeScript maintains type safety:
Object Destructuring
interface Person {
name: string;
age: number;
city: string;
}
const person: Person = { name: 'John', age: 30, city: 'Copenhagen' };
// Destructure with type inference
const { name, age, city } = person;
console.log(name); // "John" (type: string)
console.log(age); // 30 (type: number)
console.log(city); // "Copenhagen" (type: string)
Renaming Variables
interface Person {
name: string;
age: number;
}
const person: Person = { name: 'Alice', age: 25 };
const { name: personName, age: personAge } = person;
console.log(personName); // "Alice"
console.log(personAge); // 25
Default Values with Types
interface Person {
name: string;
age?: number;
city?: string;
}
const person: Person = { name: 'Bob' };
const { name, age = 30, city = 'Unknown' } = person;
console.log(name); // "Bob"
console.log(age); // 30 (default, type: number)
console.log(city); // "Unknown" (default, type: string)
Type Guards and Objects
TypeScript provides ways to check object types at runtime:
interface Dog {
breed: string;
bark(): void;
}
interface Cat {
lives: number;
meow(): void;
}
type Pet = Dog | Cat;
// Type guard function
function isDog(pet: Pet): pet is Dog {
return (pet as Dog).bark !== undefined;
}
function handlePet(pet: Pet): void {
if (isDog(pet)) {
console.log(`Dog breed: ${pet.breed}`);
pet.bark();
} else {
console.log(`Cat lives: ${pet.lives}`);
pet.meow();
}
}
const dog: Dog = {
breed: "Labrador",
bark() { console.log("Woof!"); }
};
const cat: Cat = {
lives: 9,
meow() { console.log("Meow!"); }
};
handlePet(dog); // Dog breed: Labrador, Woof!
handlePet(cat); // Cat lives: 9, Meow!
Utility Types for Objects
TypeScript provides built-in utility types for transforming object types:
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial - makes all properties optional
type PartialUser = Partial<User>;
let user1: PartialUser = { name: "Alice" }; // OK
// Required - makes all properties required
interface OptionalUser {
id?: number;
name?: string;
}
type RequiredUser = Required<OptionalUser>;
// let user2: RequiredUser = { id: 1 }; // Error: Property 'name' is missing
// Readonly - makes all properties readonly
type ReadonlyUser = Readonly<User>;
let user3: ReadonlyUser = { id: 1, name: "Bob", email: "bob@test.com", age: 30 };
// user3.name = "Alice"; // Error: Cannot assign to 'name' because it is a read-only property
// Pick - picks specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
let user4: UserPreview = { id: 1, name: "Charlie" }; // OK
// Omit - omits specific properties
type UserWithoutEmail = Omit<User, 'email'>;
let user5: UserWithoutEmail = { id: 1, name: "David", age: 25 }; // OK
// Record - creates object type with specific keys and value type
type UserRoles = Record<string, string>;
let roles: UserRoles = {
admin: "Administrator",
user: "Regular User",
guest: "Guest"
};