TypeScript interface vs type: Choose from the Concepts
Move beyond deciding which syntax is superior: derive the choice from the roles of a boundary promise and a description of valid values
What is a type? A set of values and a promise of operations
Programs work with values such as numbers, strings, booleans, and objects. A type is more than a label attached to them. A useful beginner's model is to treat a type as the set of values that may appear in a place. boolean contains true and false, while 'idle' | 'running' contains only those two listed values.
Once the range of values is known, the operations that are safe also become known. A number supports arithmetic and toFixed, while a string supports toUpperCase. While a value may still be string | number, an operation unique to one side is unavailable until the code narrows which one it is.
1. Limit possible values
A type draws a boundary between accepted and excluded values. A union can narrow the possibilities to an even smaller set.
boolean = true | false
2. Determine safe operations
The compiler uses a value's type to allow only the properties and operations known to be safe at that point in the program.
number → +, -, toFixed(...)
3. Describe required structure
An object type lists the properties a value must have and the type of each one. TypeScript checks the structure rather than requiring a particular class name.
{ id: string; active: boolean }
type TaskStatus = 'idle' | 'running' | 'stopped' let currentStatus: TaskStatus = 'idle' currentStatus = 'running' // currentStatus = 'unknown' // Error: not included in TaskStatus function formatValue(value: string | number): string { if (typeof value === 'number') { return value.toFixed(1) // An operation on number } return value.toUpperCase() // An operation on string } type UserRecord = { id: string active: boolean } const user: UserRecord = { id: 'u-1', active: true }
The TypeScript compiler compares actual and expected types at assignments, function arguments, return values, and property access. It reports contradictions before the code runs. Even without an explicit annotation, it infers types from initial values and control flow and updates what it knows.
A type is not the runtime value itself; it is the compiler's knowledge about that value. That knowledge helps find impossible values and unsafe operations before the code executes.
Type checking and runtime input validation are different
TypeScript type information is generally erased after compilation. It cannot check at runtime whether data from an API, JSON document, or form matches an annotation. External input therefore needs runtime validation in addition to types.
Separate the words before joining the debate
The question 'Should TypeScript use interface or type?' often looks like a matter of taste because both can describe the same object shape. The design concept of an interface and the general concept of a type, however, do not sit at the same level.
A type is the general concept that classifies which values are possible and which operations are allowed. An interface is more specific: it describes what an object exposes and promises across a boundary. Meanwhile, TypeScript's type keyword declares a type alias, a name for a type expression; it does not automatically create a new nominal kind of value.
Interface: a boundary promise
It lists the properties and operations visible to a user so the role can be used without knowing its implementation. Its central question is: what does this participant promise?
execute(command): void
Type: a set of valid values
It describes which values are accepted and excluded across strings, numbers, objects, functions, tuples, unions, and more. Its central question is: which values are valid?
'idle' | 'running' / readonly [number, number]
A TypeScript interface declaration also creates a type. The practical comparison is therefore not interface versus type, but naming an object type with an interface declaration versus naming a type expression with a type alias.
Why can both describe the same shape?
TypeScript uses structural typing: compatibility is based on the members a value has, not primarily on its declared name or inheritance chain. When the required properties and methods match, values can pass between an interface-named type and an object type named by a type alias.
interface InterfaceUser { id: string name: string } type AliasUser = { id: string name: string } const user: InterfaceUser = { id: 'u-1', name: 'Ada' } // Assignment works because the structures match, not the names const sameShape: AliasUser = user type Runnable = { run(): void } // A class can also implement an object type alias class TaskRunner implements Runnable { run() { /* Perform the work */ } }
Their abilities therefore overlap substantially for object shapes. The implements clause is not exclusive to interface declarations either; a class can implement an object type alias whose members are statically known.
If the only question is whether the syntax can express an object shape, the answer is often 'either.' A useful choice comes from the design intent carried by the name, not merely from syntactic capability.
Intent communicated well by interface: roles and public boundaries
An interface fits public object APIs, roles shared by several implementations, and sets of operations that classes must provide. It tells readers that this declaration is a promise to users rather than an incidental arrangement of internal data.
interface VehicleController { steer(direction: number): void accelerate(amount: number): void brake(amount: number): void } class SimulatorController implements VehicleController { steer(direction: number) { /* Move virtual wheels */ } accelerate(amount: number) { /* Increase virtual speed */ } brake(amount: number) { /* Decrease virtual speed */ } } function testDrive(controller: VehicleController) { controller.accelerate(0.4) controller.steer(0.2) controller.brake(0.3) }
VehicleController defines operations shared by a real vehicle, a simulator, or a test double. Calling code can depend on that boundary rather than a concrete class. Interface extends can also communicate a family of object contracts that grows in stages.
There is no language rule saying that wanting implements always requires interface. An object type alias may support the same check. The reason to choose interface here is primarily its intent: a boundary, a role, and potentially an extensible contract.
Where type aliases are needed: composing possible values
A type alias can name any type expression, including primitives, unions, tuples, function types, intersections, template literal types, mapped types, and conditional types. When the work centers on constructing a set of valid values, type is the natural tool.
type VehicleId = string type Position = readonly [x: number, y: number] type DriveCommand = | { kind: 'steer'; direction: number } | { kind: 'accelerate'; amount: number } | { kind: 'brake'; amount: number } type CommandHandler = (command: DriveCommand) => Promise<void> type ReadonlyFields<T> = { readonly [Key in keyof T]: T[Key] }
The DriveCommand union says a command is one of three alternatives, each with different required data. Position is a tuple with fixed order and length. These describe the shapes values may take rather than a public boundary of an object. A type alias is still only an alias, so it does not automatically create a nominally distinct type from an identical structure.
Use them together instead of choosing a side
In practical design, interface and type are not competitors. An interface can describe the capability an object exposes, while a type alias describes the alternatives in data passed to that capability. Each concept then has a distinct job.
// Use a type alias for the set of possible commands type DriveCommand = | { kind: 'steer'; direction: number } | { kind: 'accelerate'; amount: number } | { kind: 'brake'; amount: number } // Use an interface for the operation promised to callers interface VehicleController { execute(command: DriveCommand): void } class SimulatorController implements VehicleController { execute(command: DriveCommand) { switch (command.kind) { case 'steer': return this.steer(command.direction) case 'accelerate': return this.accelerate(command.amount) case 'brake': return this.brake(command.amount) } } private steer(direction: number) { /* ... */ } private accelerate(amount: number) { /* ... */ } private brake(amount: number) { /* ... */ } }
Here DriveCommand is a closed union of valid commands, while VehicleController promises that an object can execute such a command. This separates the axis for adding implementations from the axis for adding commands.
Use interface for 'who promises what' and type for 'which combinations of values are valid.' Once that distinction is visible, the choice becomes a design decision rather than a style faction.
A real difference: can the declaration be reopened?
Compatible interface declarations with the same name are merged into one declaration. A type alias cannot be redeclared under the same name. This is not merely cosmetic; it determines whether a named type is designed to accept additions from elsewhere.
interface Logger { info(message: string): void } interface PluginContext { logger: Logger } // A declaration with the same name adds to the existing interface interface PluginContext { locale: 'ja' | 'en' } const context: PluginContext = { logger: console, locale: 'ja' } type PluginOptions = { debug: boolean } // type PluginOptions = { locale: string } // Error: Duplicate identifier 'PluginOptions'
interface: declarations can reopen
Library declarations and module augmentation can add members to an existing public contract. This is useful when an API intentionally provides extension points.
type: the same name cannot be redeclared
Members are not implicitly added to the same name later. Intersections and new aliases can still compose an explicitly named new type expression.
Declaration merging is valuable for library extension points, but accidentally repeated interface names can also expand a contract. Openness is not automatically desirable inside every application; consider who is supposed to extend the declaration.
Both styles can build on existing shapes: interface uses extends, while type aliases commonly use intersections. Conflicting properties are not handled identically, however. Consider where you want an incompatible extension to become an error, not just which syntax is shorter.
A practical decision table
These are heuristics that begin with intent, not absolute rules. If a project already has a consistent convention and both forms express the design equally well, following that convention usually improves readability.
| What you are modeling | First choice | Reason |
|---|---|---|
| A public object API or service boundary | interface | Clearly communicates a promise to users, a role independent of implementation, and a family of contracts through extends. |
| A role fulfilled by replaceable class implementations | interface | Reads naturally with implements as a capability a class promises. A type alias may still be technically valid. |
| A union, tuple, primitive alias, or function type | type | A type alias can name arbitrary type expressions beyond object boundaries. |
| A mapped, conditional, or template literal type | type | Computing and composing a new type expression from existing types belongs to type aliases. |
| A simple object shape used only inside the application | either | Both work; prefer the extension policy, local convention, and intent conveyed by the name. |
| A public type requiring declaration merging or module augmentation | interface | Merging compatible declarations under the same name is a requirement in this case. |
When uncertain, change the question. Do not ask which syntax is more popular; ask whether the name promises a capability across a boundary or constructs a set of valid values. Prefer interface for the former, type for the latter, and project consistency for an otherwise ordinary overlapping shape.
Common misconceptions
Using interface does not make a type nominal. TypeScript usually checks structural compatibility, so a value can conform when it has the required members even without an explicit implements declaration.
The right side of implements is not limited to interface declarations. An object type alias with statically known members can be used too. The case for interface is design intent, not a universal syntax requirement.
type is not simply newer while interface is obsolete, nor is either one always more powerful. Their capabilities overlap, and each also has areas the other cannot express or support in the same way.
Neither an interface nor a type alias validates input at runtime. TypeScript type information is generally erased during compilation, so external data still needs separate runtime validation.
Summary
A type is the general concept that classifies valid values and operations. An interface is the design concept of a promise exposed by an object across a boundary.
A TypeScript interface declaration creates a type, while the type keyword names an arbitrary type expression. The debate is not about types versus non-types.
Structural typing lets both forms describe ordinary object shapes, and implements is not exclusive to interface declarations.
Interface is natural for public boundaries, roles, and declaration merging; type is natural for unions, tuples, and type-level composition. They work together in one design.
When either form works, prioritize extension policy, project convention, and the intent the name communicates over claims that one syntax always wins.
Official references
The language behavior described here follows these sections of the official TypeScript Handbook.