Skip to main content

Command Palette

Search for a command to run...

A Comprehensive Guide to Types, Interfaces, Classes, and Abstract Classes in TypeScript

Updated
8 min read
M

Software Engineer from Egypt specializing in backend development.

When working with TypeScript, one of the first hurdles developers face is understanding the differences between types, interfaces, abstract classes, and classes. At first glance, they all seem to overlap—each can describe shapes, enforce contracts, or structure code. But under the hood, they serve very different purposes, with distinct rules for compile-time safety, runtime behavior, inheritance, and extensibility.

This article breaks down these four building blocks side by side. It starts with a quick comparison table for a high-level snapshot, then dives deeper into when and why you’d use each—complete with examples, pros and cons, and common pitfalls. By the end, you’ll have a clear mental model for choosing the right tool in different scenarios, whether you’re defining flexible data shapes, designing library APIs, or implementing real-world application logic.

Quick Overview

FeatureRuntime ExistenceCan InstantiateImplementationMultiple Inheritance/ExtendsExtensibilityCommon Pitfalls/Misconceptions
TypeNo (erased at compile-time)NoNo (purely descriptive)Yes (via intersections: &)Can be extended via intersections or unionsOften confused with interfaces; types can't be “implemented” by classes but can describe them.
InterfaceNo (erased at compile-time)NoNo (declares shape only)Yes (classes can implement multiple; interfaces can extend multiple)Supports declaration merging (e.g., multiple files can add to the same interface)Better for object literals; avoids some type alias recursion issues in large projects.
Abstract ClassYes (transpiles to JS class)No (requires subclass)Partial (mix of abstract and concrete methods/properties)No (single extends, but can implement multiple interfaces)Can be extended by subclasses; useful for enforced inheritanceNot truly “abstract” in JS runtime—subclasses must implement abstracts, but runtime checks are limited.
ClassYes (transpiles to JS class or function)YesFull (concrete methods/properties)No (single extends, but can implement multiple interfaces)Can extend one class and implement interfaces; supports decoratorsRuntime behavior means they're heavier; use sparingly if pure types suffice.

This table offers a quick overview, showing how TypeScript converts to JavaScript. For example, types and interfaces vanish at runtime, while classes remain.

When to Use Each (With Details and Examples)

Types

  • Details: Types are aliases for other types and excel at composing complex shapes using TypeScript's advanced features like mapped types, conditional types, and template literals. They're purely for type checking—no runtime impact. Use them when you need flexibility beyond simple objects, like for primitives, functions, or generics. They're also great for utility types (e.g., Partial<T>, Readonly<T> built into TypeScript).

  • Pros: Highly composable; support for unions/intersections make them ideal for modeling variants or merging schemas.

  • Cons: No declaration merging (unlike interfaces); can lead to recursion errors in deeply nested types.

  • Examples:

    • Unions for enums-like behavior:

        type Status = "active" | "inactive" | "pending";
        function setStatus(s: Status) { /* ... */ }
        setStatus("active"); // OK
        setStatus("deleted"); // Error: Type '"deleted"' is not assignable to type 'Status'
      
    • Intersections for combining types:

        type User = { id: number; name: string };
        type Role = { role: "admin" | "user" };
        type UserWithRole = User & Role;
        const admin: UserWithRole = { id: 1, name: "Alice", role: "admin" }; // OK
      
    • Complex computed types:

        type User = { id: number; name: string };
      
        type Keys = "id" | "name";
        type MyPick<T, K extends keyof T> = { [P in K]: T[P] }; // custom Pick utility
        type UserId = MyPick<User, "id">; // { id: number }
      
    • When to prefer over interfaces: For non-object shapes or when using conditionals (e.g., type Result<T> = T extends string ? Uppercase<T> : T).

Interfaces

  • Details: Interfaces define contracts for objects, functions, or classes. They're extensible via “declaration merging,” where TypeScript automatically combines multiple declarations of the same interface (a unique advantage, especially in libraries or large codebases where modules can augment shared interfaces without conflicts). They generally perform better than types for object shapes due to TypeScript's caching mechanisms, which optimize repeated type checks in complex projects.

  • Pros: Easier to extend/merge; classes can implement them; clearer error messages in some cases; performance edge for large object graphs.

  • Cons: Limited to object-like shapes (no unions/intersections directly—use extends for similar effects).

  • Examples:

    • Object shapes:

        interface Person {
          name: string;
          age: number;
        }
        const alice: Person = { name: "Alice", age: 30 }; // OK
      
    • Extensibility with declaration merging (e.g., in separate files):

        // file1.ts
        interface Window {
          customProp: string;
        }
        // file2.ts
        interface Window {
          anotherProp: number;
        }
        // Merged automatically: Window has both customProp and anotherProp
      
    • API definitions:

        interface ApiResponse {
          data: unknown;
          status: number;
        }
        function fetchData(): ApiResponse { /* ... */ }
      
    • When to prefer over types: For public APIs or team collaboration, as merging allows additive changes without breaking code; also for better compile-time performance in object-heavy codebases.

Abstract Classes

  • Details: These provide a base for subclasses, allowing shared code (concrete methods/properties) while forcing subclasses to implement abstracts. They support constructors, private/protected members, and runtime state. Unlike interfaces, they exist at runtime, so they're useful for polymorphism in inheritance chains.

  • Pros: Enforce structure with shared implementation; great for frameworks (e.g., Angular services).

  • Cons: Single inheritance limits flexibility; can lead to deep hierarchies if overused (favor composition over inheritance).

  • Examples:

    • Shared implementation:

        abstract class Animal {
          constructor(protected name: string) {}
          abstract makeSound(): void; // Must be implemented by subclasses
          move() { console.log(`${this.name} is moving`); } // Concrete method
        }
        class Dog extends Animal {
          makeSound() { console.log("Woof!"); }
        }
        const dog = new Dog("Buddy");
        dog.move(); // "Buddy is moving"
        dog.makeSound(); // "Woof!"
        // new Animal("Abstract"); // Error: Cannot create an instance of an abstract class
      
    • Mix of abstract/concrete:

        abstract class Logger {
          abstract log(message: string): void;
          error(message: string) { this.log(`ERROR: ${message}`); } // Uses abstract method
        }
      
    • When to use: For families of related classes (e.g., shapes in a graphics library: Circle and Square extend AbstractShape with a shared draw() method).

Classes

  • Details: Full-fledged objects with runtime behavior, including constructors, methods, getters/setters, and access modifiers (public, private, protected, readonly). These modifiers are a key differentiator from interfaces (which don't support them), enabling better encapsulation and control over member visibility/inheritability. Classes can extend one class and implement multiple interfaces. Use for anything that needs instantiation and state management.

  • Pros: Encapsulation and polymorphism; integrates with JS ecosystem (e.g., prototypes); access modifiers prevent unintended access/mutation.

  • Cons: Runtime overhead; not ideal for pure data shapes (use interfaces/types instead).

  • Examples:

    • Basic class with constructor and access modifiers:

        class User {
          private id: number;
          readonly createdAt: Date;
          constructor(public name: string, protected email: string) {
            this.id = Math.random(); // Constructor logic
            this.createdAt = new Date(); // Readonly: can't be reassigned
          }
          greet() { return `Hello, ${this.name}`; }
        }
        const user = new User("Alice", "alice@example.com");
        console.log(user.greet()); // "Hello, Alice"
        // user.id; // Error: Property 'id' is private
        // user.createdAt = new Date(); // Error: Cannot assign to 'createdAt' because it is a read-only property
      
    • Inheritance and interfaces:

        interface Printable { print(): void; }
        class MyDocument extends User implements Printable { // Extends class, implements interface
          print() { console.log("Printing..."); }
        }
      
    • When to use: For services, models with logic (e.g., a DatabaseConnection class that manages connections).

Key Mental Model

  • Types: Like sticky notes labeling data—”this must look like X or Y.” They're declarative and flexible, ideal for functional programming styles where you compose types without runtime concerns.

  • Interfaces: Like legal contracts specifying requirements—”you must provide these properties/methods.” They're about promises and compatibility, promoting loose coupling.

  • Abstract Classes: Like half-built houses with foundations and some rooms finished—you add the rest. They bridge design (abstracts) and implementation (concretes), enforcing a family resemblance.

  • Classes: Like fully constructed, livable houses—you move in and use them. They're about creating real objects with behavior and state.

A key insight: Types/interfaces are for structural typing (duck typing: if it quacks like a duck...), while classes/abstract classes add nominal elements via inheritance. In practice, mix them—e.g., a class implements an interface described by a type.

Best Practice Rule of Thumb

  1. Start with interfaces for object shapes—they're more idiomatic in TypeScript docs and ecosystems (e.g., React props), and generally perform better than types due to caching mechanisms.

  2. Use types when interfaces can't handle it (unions, intersections)—or for quick aliases. But if performance matters in huge projects, interfaces might edge out due to caching in the compiler.

  3. Use abstract classes when you need to share code between similar classes—but avoid if composition (e.g., via mixins or functions) suffices, to prevent brittle hierarchies.

  4. Use classes for everything you actually want to create instances of—and leverage TypeScript features like parameter properties (e.g., constructor(public name: string)) for brevity, along with access modifiers for encapsulation.

Additional tips:

  • Migration/Refactoring: If a class has no state/methods, refactor to an interface/type for lighter code.

  • Generics: All support generics (e.g., interface List<T>, type Pair<T> = [T, T], abstract class Base<T>).

  • Common Mistake: Don't use classes for namespaces—use modules instead.

  • Performance: For massive type unions, types might bloat compile times; profile if needed. Interfaces' declaration merging shines in extensible libraries.

  • Ecosystem Fit: In libraries, provide interfaces for users to implement or extend.

Conclusion

Mastering the differences between types, interfaces, abstract classes, and classes is less about memorizing rules and more about knowing which tool best fits the problem at hand. Types and interfaces shine at compile-time, letting you model and validate shapes with zero runtime cost. Abstract classes and classes, on the other hand, bridge into JavaScript’s runtime world, giving you structure, encapsulation, and actual behavior.

The bottom line: let types/interfaces guide the shape of your data, and reach for abstract classes/classes when you need runtime behavior. With this mental model, you can choose with confidence—and focus on building great software instead of wrestling with the type system.

study notes

Part 1 of 1

In this series, I will publish my notes about topics I've studied.