PGH Web

PGH Web

Web Solutions from Pittsburgh

TypeScript Interfaces vs Types: What Actually Differs

Jeff Straney·

I wrote a library interface once and spent an hour debugging why consumers couldn't extend it. I had used type. Someone tried to add a property and the augmentation didn't work. I was confused until I realized that interface supports declaration merging and type does not. I had picked the wrong tool and cost someone three hours of their afternoon because the docs didn't make it obvious when to reach for which.

TypeScript has two ways to describe the shape of an object: interface and type. If you come from a language with structs, the distinction isn't obvious and the documentation doesn't help as much as you'd expect. Most explanations either oversimplify ("use interface for objects") or produce a feature matrix that doesn't tell you what to do with it. The real answer is: the choice matters when it matters, which is not most of the time.

Reach for interface when you're describing the shape of something other code will implement or extend. Reach for type when you need unions, intersections, computed property keys, or anything interface can't express. They overlap in the middle, and there you can pick either.

What interface does

An interface describes the shape of an object and declares a contract that anything implementing it must satisfy:

interface Parser {
  parse(input: string): Record<string, unknown>;
  validate(data: Record<string, unknown>): boolean;
}

// A YAML parser that implements the interface
class YamlParser implements Parser {
  parse(input: string) {
    // parse YAML
  }
  validate(data) {
    // validate YAML structure
  }
}

// A JSON parser that also implements it
class JsonParser implements Parser {
  parse(input: string) {
    // parse JSON
  }
  validate(data) {
    // validate JSON structure
  }
}

The part that surprises people from Go or C#: TypeScript interfaces are structural, not nominal. A class doesn't have to declare that it implements Parser. If it has the right shape, it satisfies Parser whether it says so or not. Any object with parse and validate methods satisfies Parser.

You can add interfaces to code you don't control. You can also end up with two completely unrelated interfaces that are compatible because their shapes happen to match.

What type does

type is more expressive. It can describe everything interface can, plus:

type StringOrNumber = string | number;
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Keys = keyof SomeType;
type Nullable<T> = T | null;

Union types require type. Intersections work with both (interface via extends, type via &), but type is often cleaner for ad-hoc intersections:

type AdminUser = User & { permissions: string[] };

Where they diverge

If you declare the same interface twice, TypeScript merges them. This doesn't work with type. Merging is how @types/node adds properties to global objects like Window or process without touching the original declarations. If you're writing a library that lets consumers extend your types, only interface supports this.

type handles mapped and computed keys. interface has limited support:

type Flags = {
  [Key in "a" | "b" | "c"]: boolean;
};
// The equivalent interface syntax doesn't compile.

There's also a difference in error output. TypeScript often shows the expanded form of a type alias inline, which can get long. Interfaces show by name, which is usually easier to read when something goes wrong.

The "struct" comparison breaks down

Coming from Go, you might expect TypeScript interface to work like a Go interface: you declare it, and types explicitly implement it. It doesn't.

In Go, var _ io.Writer = (*MyWriter)(nil) confirms a type satisfies an interface at compile time. In TypeScript, if the shape matches, it matches. You don't need to declare the relationship. TypeScript will accept an object that was defined with no knowledge of your interface, as long as the fields line up.

Once you internalize structural typing, the interface vs type question gets less interesting. You're not picking a nominal contract. You're picking a syntax for a shape. Use interface for what you're publishing or expecting others to extend. Use type for everything else.