Skip to content

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:

interface PersonData {
    name: string;
    birthYear: number;
}

interface Person extends PersonData {
    estimatedAge(): number;
    toString(): void;
}

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"
};