Skip to main content

Practical Uses and Examples of TypeScript Object Types

Defining the structure of objects in TypeScript is made easier with object types. These allow you to specify an object's shape, including its properties and their types. Let's explore how to use object types effectively, from the basics to more advanced features and real-world applications.

When building full-stack applications, object types become even more valuable as they help maintain consistency between your frontend and backend.

How to Define an Object Type in TypeScript

In TypeScript, you can define an object type using either the interface keyword or the type alias. Here's a simple example using interface:

interface Person {
name: string;
age: number;
}

Alternatively, you can use a type alias:

type Person = {
name: string;
age: number;
};

Both approaches accomplish similar goals, but interfaces are often preferred when defining object shapes because they can be extended and implemented by classes. Type aliases offer more flexibility for creating union types and other advanced type constructs.

When working with a Convex backend, you'll often use object types to define your database schema, ensuring type safety across your entire application.

You can use your defined object types to create type-safe objects:

// Using the Person interface
const alice: Person = {
name: "Alice",
age: 30
};

// TypeScript will catch errors at compile time
const bob: Person = {
name: "Bob"
// Error: Property 'age' is missing in type '{ name: string; }' but required in type 'Person'
};

Creating Complex Object Types in TypeScript

To build a complex object type with nested structures, you can employ nested interfaces or type aliases. Here's how:

Using interfaces:

interface Address {
street: string;
city: string;
state: string;
zip: string;
}

interface User {
name: string;
age: number;
address: Address;
}

Or with type aliases:

type Address = {
street: string;
city: string;
state: string;
zip: string;
};

type User = {
name: string;
age: number;
address: Address;
};

You can also include arrays and other complex types:

interface User {
name: string;
age: number;
address: Address;
phoneNumbers: string[];
settings: {
notifications: boolean;
theme: "light" | "dark";
};
}

When working with Convex, complex object types are useful for modeling your database schema, where you often need nested structures to represent relationships between entities.

Extending Object Types in TypeScript

To add more properties to an object type, use the extends keyword or the & operator. For example, to add an address property to the Person type:

Using extends with interfaces:

interface Person {
name: string;
age: number;
}

interface Employee extends Person {
employeeId: string;
department: string;
}

// Now Employee has name, age, employeeId, and department

Using the intersection operator (&) operator with type aliases:

type Person = {
name: string;
age: number;
};

type Employee = Person & {
employeeId: string;
department: string;
};

You can also extend multiple types at once:

interface HasAddress {
address: {
street: string;
city: string;
};
}

interface Employee extends Person, HasAddress {
employeeId: string;
}

// Employee now has name, age, address, and employeeId

This extension capability is useful when working with Convex's document model, where you might want to create specific document types that share common fields while adding domain-specific properties.

Ensuring Object Property Type Safety

TypeScript offers several ways to enforce constraints on object types, ensuring your data maintains integrity:

Readonly Properties

To make properties immutable, use the readonly keyword:

interface Person {
readonly name: string;
age: number;
}

const person: Person = {
name: "Alice",
age: 30
};

// This will cause a TypeScript error
person.name = "Bob"; // Error: Cannot assign to 'name' because it is a readonly property

Optional Properties

To make properties optional, use the optional chaining operator (?):

interface Person {
name: string;
age?: number; // age is optional
}

// Both are valid
const alice: Person = { name: "Alice", age: 30 };
const bob: Person = { name: "Bob" }; // No age property

String Index Signatures

For objects with dynamic keys:

interface Dictionary {
[key: string]: string;
}

const colors: Dictionary = {
red: "#FF0000",
green: "#00FF00",
blue: "#0000FF"
};

Type Literals

For fixed sets of allowed values:

interface User {
role: "admin" | "user" | "guest";
}

Tthese type constraints help validate data before it reaches your database, preventing invalid states.

Transforming Object Types with Mapped Types

TypeScript map type operations let you transform existing object types into new ones by applying operations to each property. This is powerful for creating variations of your types without duplicating definitions:

Making All Properties Readonly

interface Person {
name: string;
age: number;
}

type ReadonlyPerson = {
readonly [P in keyof Person]: Person[P];
};

// Equivalent to:
// interface ReadonlyPerson {
// readonly name: string;
// readonly age: number;
// }

Transforming Property Types

You can transform the types of properties:

interface Person {
name: string;
age: number;
}

type StringifiedPerson = {
[P in keyof Person]: string;
};

// All properties are now strings
// Equivalent to:
// interface StringifiedPerson {
// name: string;
// age: string;
// }

Adding or Removing Modifiers

You can add or remove optional and readonly modifiers:

// Make all properties optional
type PartialPerson = {
[P in keyof Person]?: Person[P];
};

// Remove readonly from properties
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

Convex's type system works well with mapped types, allowing you to create flexible filters and transformations for your data models.

Managing Optional Properties in a TypeScript Object Type

Optional properties let you define objects that might not have all fields present. This is useful for forms, API responses, and partial updates:

Individual Optional Properties

Use the question mark (?) to make specific properties optional:

interface UserProfile {
username: string; // Required
email: string; // Required
bio?: string; // Optional
website?: string; // Optional
preferences?: {
notifications?: boolean;
theme?: "light" | "dark";
};
}

Making All Properties Optional

The Partial<T> utility type makes all properties of an object type optional:

interface User {
id: string;
name: string;
email: string;
}

type PartialUser = Partial<User>;

// Equivalent to:
// interface PartialUser {
// id?: string;
// name?: string;
// email?: string;
// }

Working with Optional Properties

When using objects with optional properties, you'll often need to check if they exist:

function updateUser(user: User, updates: Partial<User>) {
// Safe way to update user with optional fields
return {
...user,
...updates
};
}

// Safe property access with optional chaining
function getUserTheme(user: UserProfile): string {
return user.preferences?.theme || "default";
}

When building forms with Convex, optional properties help you distinguish between fields that need validation and those that can be omitted

Manipulating Object Types with Utility Types

TypeScript utility types provide built-in type transformations that save you from writing complex mapped types yourself. These tools help you manipulate object types with minimal code:

Basic Utility Types

interface User {
id: string;
name: string;
email: string;
role: "admin" | "user";
createdAt: Date;
}

// Make all properties optional
type PartialUser = Partial<User>;

// Make all properties readonly
type ReadonlyUser = Readonly<User>;

// Pick specific properties
type UserCredentials = Pick<User, 'id' | 'email'>;

// Omit specific properties
type PublicUser = Omit<User, 'id' | 'email'>;

// Extract just the role property type
type UserRole = User['role']; // "admin" | "user"

Combining Utility Types

You can combine utility types for more complex transformations:

// Create a type with only some optional fields
type UpdateableUser = Pick<User, 'name' | 'email'> & Partial<Pick<User, 'role'>>;

// Create a readonly version with only specific fields
type ReadonlyUserCredentials = Readonly<Pick<User, 'id' | 'email'>>;

Record Type for Key-Value Objects

The Record<K, T> utility type creates an object type with specific keys and value types:

// Create a dictionary of user objects by ID
type UsersById = Record<string, User>;

const users: UsersById = {
"user_1": { id: "user_1", name: "Alice", email: "alice@example.com", role: "admin", createdAt: new Date() },
"user_2": { id: "user_2", name: "Bob", email: "bob@example.com", role: "user", createdAt: new Date() }
};

When working with Convex's schema definitions, utility types help create reusable validators for your API's input and output types.

Final Thoughts on TypeScript Object Types

TypeScript object types give your code structure and help catch errors before they reach production. With interfaces, type aliases, and utility types, your objects become predictable and maintainable. As you build with TypeScript, keep these patterns in mind:

  • Use interfaces for objects that might be extended or implemented by classes
  • Use type aliases for complex types, unions, and intersections
  • Make properties readonly when they shouldn't change
  • Apply utility types to transform existing types without duplication

Ready to apply these skills in a full-stack TypeScript application? Check out Convex's end-to-end TypeScript support to see how object types create a seamless development experience from your database to your UI.