Skip to main content
Interfaces vs Types

Objects & Interfaces

Objects are the core of JavaScript, and TypeScript provides powerful ways to describe their shapes. Interfaces and type aliases are your primary tools for defining object structures.

1. Interfaces

Interfaces define contracts for object shapes. They’re the most common way to type objects in TypeScript.

Basic Interface

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

const user: User = {
  id: 1,
  name: 'Alice',
  email: '[email protected]'
};

// Missing property error
const badUser: User = {
  id: 2,
  name: 'Bob'
  // ❌ Error: Property 'email' is missing
};

Optional Properties

interface Config {
  host: string;
  port: number;
  debug?: boolean;      // Optional
  timeout?: number;     // Optional
}

const config: Config = {
  host: 'localhost',
  port: 3000
  // debug and timeout are optional
};

Readonly Properties

interface Point {
  readonly x: number;
  readonly y: number;
}

const point: Point = { x: 10, y: 20 };
// point.x = 5; // ❌ Error: Cannot assign to 'x' because it is read-only

// ReadonlyArray
const numbers: readonly number[] = [1, 2, 3];
// numbers.push(4); // ❌ Error

2. Type Aliases vs Interfaces

Both can describe object shapes. Use interfaces for objects that will be extended; use type aliases for unions, tuples, or complex types.

Syntax Differences

// Interface
interface UserInterface {
  name: string;
  age: number;
}

// Type Alias
type UserType = {
  name: string;
  age: number;
};

// Both work the same for objects
const user1: UserInterface = { name: 'Alice', age: 25 };
const user2: UserType = { name: 'Bob', age: 30 };

Key Differences

// 1. Interfaces can be extended (declaration merging)
interface Animal {
  name: string;
}

interface Animal {
  age: number;
}

// Animal now has both name and age
const pet: Animal = { name: 'Fluffy', age: 3 };

// 2. Type aliases can represent primitives, unions, tuples
type ID = string | number;
type Point = [number, number];
type Name = string;

// 3. Type aliases can use mapped types
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};
When to use which?
  • Use interfaces for objects that might be extended or implemented
  • Use type aliases for unions, intersections, tuples, or complex compositions
  • Both work for most object types; pick one style and be consistent

3. Extending Interfaces

Build complex types by extending existing ones.

Single Inheritance

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

interface Dog extends Animal {
  breed: string;
  bark(): void;
}

const dog: Dog = {
  name: 'Max',
  age: 3,
  breed: 'Golden Retriever',
  bark() {
    console.log('Woof!');
  }
};

Multiple Inheritance

interface Printable {
  print(): void;
}

interface Loggable {
  log(): void;
}

interface Document extends Printable, Loggable {
  title: string;
  content: string;
}

const doc: Document = {
  title: 'Report',
  content: 'Lorem ipsum...',
  print() {
    console.log(this.content);
  },
  log() {
    console.log(`Document: ${this.title}`);
  }
};

Extending Type Aliases

// Types can extend interfaces
type Animal = {
  name: string;
};

interface Pet extends Animal {
  owner: string;
}

// Interfaces can extend types
interface Bird {
  fly(): void;
}

type Parrot = Bird & {
  speak(): void;
};

4. Intersection Types

Combine multiple types into one.
type Person = {
  name: string;
  age: number;
};

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

// Intersection: has all properties from both
type Staff = Person & Employee;

const staff: Staff = {
  name: 'Alice',
  age: 30,
  employeeId: 'E001',
  department: 'Engineering'
};

Intersection with Conflicts

type A = { value: string };
type B = { value: number };

type C = A & B;
// C.value is of type 'never' (string & number = never)

// This is more common with method signatures
type WithTimestamp = {
  createdAt: Date;
};

type User = {
  name: string;
} & WithTimestamp;

const user: User = {
  name: 'Alice',
  createdAt: new Date()
};

5. Index Signatures

Define types for objects with dynamic keys.

String Index Signature

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

const colors: StringDictionary = {
  red: '#ff0000',
  green: '#00ff00',
  blue: '#0000ff'
};

// Any string key works
colors['purple'] = '#800080';
colors.orange = '#ffa500';

Number Index Signature

interface NumberArray {
  [index: number]: string;
}

const arr: NumberArray = ['a', 'b', 'c'];
console.log(arr[0]); // 'a'

Mixing Index Signatures with Properties

interface User {
  id: number;
  name: string;
  [key: string]: string | number; // Must be compatible
}

const user: User = {
  id: 1,
  name: 'Alice',
  email: '[email protected]',
  age: 25
};
When using index signatures, all explicit properties must have types compatible with the index signature.

6. Mapped Types

Transform existing types into new ones.

Basic Mapped Type

type User = {
  name: string;
  age: number;
  email: string;
};

// Make all properties optional
type PartialUser = {
  [K in keyof User]?: User[K];
};

// Make all properties readonly
type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

Built-in Utility Types

TypeScript provides several utility types based on mapped types:
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// Partial<T> - All properties optional
type UpdateUser = Partial<User>;

// Required<T> - All properties required
type CompleteUser = Required<UpdateUser>;

// Readonly<T> - All properties readonly
type ImmutableUser = Readonly<User>;

// Pick<T, K> - Select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;

// Omit<T, K> - Exclude specific properties
type PublicUser = Omit<User, 'password'>;

// Record<K, V> - Object with keys K and values V
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;

7. Template Literal Types

Create types from string patterns.
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiPath = '/users' | '/products' | '/orders';

// Combine with template literals
type ApiEndpoint = `${HttpMethod} ${ApiPath}`;
// 'GET /users' | 'GET /products' | 'GET /orders' | 'POST /users' | ...

// Transformations
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type FocusEvent = EventName<'focus'>; // 'onFocus'

Practical Example: CSS Properties

type CSSUnit = 'px' | 'em' | 'rem' | '%';
type CSSValue = `${number}${CSSUnit}`;

interface CSSProperties {
  width: CSSValue;
  height: CSSValue;
  margin: CSSValue;
}

const styles: CSSProperties = {
  width: '100px',
  height: '50%',
  margin: '1rem'
};

8. Recursive Types

Types that reference themselves.
// JSON value type
type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue };

const json: JsonValue = {
  name: 'Alice',
  age: 25,
  active: true,
  friends: ['Bob', 'Charlie'],
  metadata: {
    nested: {
      deeply: 'value'
    }
  }
};

// Tree structure
interface TreeNode<T> {
  value: T;
  children?: TreeNode<T>[];
}

const tree: TreeNode<string> = {
  value: 'root',
  children: [
    { value: 'child1' },
    {
      value: 'child2',
      children: [{ value: 'grandchild' }]
    }
  ]
};

9. Interface for Functions and Arrays

Interfaces can describe more than plain objects.

Function Interface

interface SearchFunction {
  (query: string, limit?: number): string[];
}

const search: SearchFunction = (query, limit = 10) => {
  // Implementation
  return [`Result for: ${query}`];
};

Array Interface

interface StringArray {
  [index: number]: string;
  length: number;
  push(item: string): number;
}

// Extending Array
interface NumberList extends Array<number> {
  sum(): number;
}

Hybrid Types

interface Counter {
  (start: number): void;  // Callable
  count: number;          // Property
  reset(): void;          // Method
}

function createCounter(): Counter {
  const counter = function (start: number) {
    counter.count = start;
  } as Counter;

  counter.count = 0;
  counter.reset = function () {
    counter.count = 0;
  };

  return counter;
}

const c = createCounter();
c(10);
console.log(c.count); // 10
c.reset();
console.log(c.count); // 0

10. Practical Patterns

Builder Pattern

interface UserBuilder {
  setName(name: string): this;
  setEmail(email: string): this;
  setAge(age: number): this;
  build(): User;
}

interface User {
  name: string;
  email: string;
  age?: number;
}

class UserBuilderImpl implements UserBuilder {
  private user: Partial<User> = {};

  setName(name: string): this {
    this.user.name = name;
    return this;
  }

  setEmail(email: string): this {
    this.user.email = email;
    return this;
  }

  setAge(age: number): this {
    this.user.age = age;
    return this;
  }

  build(): User {
    if (!this.user.name || !this.user.email) {
      throw new Error('Name and email are required');
    }
    return this.user as User;
  }
}

const user = new UserBuilderImpl()
  .setName('Alice')
  .setEmail('[email protected]')
  .setAge(25)
  .build();

Repository Pattern

interface Repository<T, ID> {
  findById(id: ID): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(entity: Omit<T, 'id'>): Promise<T>;
  update(id: ID, entity: Partial<T>): Promise<T>;
  delete(id: ID): Promise<boolean>;
}

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

class UserRepository implements Repository<User, number> {
  private users: User[] = [];

  async findById(id: number): Promise<User | null> {
    return this.users.find(u => u.id === id) || null;
  }

  async findAll(): Promise<User[]> {
    return [...this.users];
  }

  async create(entity: Omit<User, 'id'>): Promise<User> {
    const user = { ...entity, id: Date.now() };
    this.users.push(user);
    return user;
  }

  async update(id: number, entity: Partial<User>): Promise<User> {
    const index = this.users.findIndex(u => u.id === id);
    if (index === -1) throw new Error('User not found');
    this.users[index] = { ...this.users[index], ...entity };
    return this.users[index];
  }

  async delete(id: number): Promise<boolean> {
    const index = this.users.findIndex(u => u.id === id);
    if (index === -1) return false;
    this.users.splice(index, 1);
    return true;
  }
}

Summary

ConceptExample
Interfaceinterface User { name: string }
Optional Property{ name?: string }
Readonly Property{ readonly id: number }
Extendinginterface Dog extends Animal
Intersectiontype Staff = Person & Employee
Index Signature{ [key: string]: number }
Mapped Types{ [K in keyof T]: T[K] }
Template Literalstype Event = `on${string}`
Utility TypesPartial<T>, Pick<T, K>, Omit<T, K>
Next, we’ll explore classes and object-oriented programming in TypeScript!