Interfaces 101: Separating Controls from Machinery
Understand the promise visible from the outside through a car's steering wheel and pedals and a television remote, then connect it to encapsulation, replaceable implementations, and polymorphism
Interfaces in everyday life
To drive a car, a driver turns the steering wheel and presses the accelerator or brake. The driver does not directly operate the steering linkage, fuel injection, motors, or hydraulics. Even when the model or power source changes, the basic controls presented to the driver remain familiar.
A television remote works the same way. The viewer uses buttons for power, volume, and channels. They can complete the operation without knowing how an infrared or radio signal is sent or how the television's circuits respond.
Car controls
The driver uses a common control scheme while different mechanisms inside each vehicle carry out those operations in their own way.
Steer
Stop
Go
Television remote
The button names and signal rules form an operational promise connecting the viewer and the television.
An interface is a promise about how to operate something, placed at the boundary between a user and its internals. The user needs to know only that promise, not the internal mechanism, to achieve a goal. Here, interface does not mean only a screen; it means any point where people and machines or software components meet.
In software, model what can be done
A software component's user needs a promise about which operations can be called, which values are accepted, and which values are returned. A TypeScript interface expresses that promise as a type.
// Declare only the operations promised to the user interface CarControls { steer(direction: number): void accelerate(amount: number): void brake(amount: number): void } interface RemoteControllable { power(): void volumeUp(): void selectChannel(channel: number): void }
CarControls declares only that steer, accelerate, and brake are available. RemoteControllable declares only power, volume, and channel operations. Calling code can handle any object that fulfills the promise in the same way.
Crucially, these declarations do not prescribe how an engine, motor, infrared transmitter, or display must be implemented. Type compatibility checks the shape of an operation; it does not prove that an implementation produces the correct result.
Encapsulation: hide internals and protect valid state
If an interface is the promise shown on the outside, encapsulation keeps implementation details and state inside the boundary. Users cannot arbitrarily rewrite internal state; they work through the published operations.
class ElectricCar implements CarControls { #speed = 0 #batteryPercent = 80 steer(direction: number) { if (direction < -1 || direction > 1) { throw new RangeError('direction must be between -1 and 1') } // Operate the internal steering mechanism } accelerate(amount: number) { if (amount < 0 || amount > 1) { throw new RangeError('amount must be between 0 and 1') } if (this.#batteryPercent === 0) return this.#speed += amount * 5 this.#batteryPercent -= 1 } brake(amount: number) { if (amount < 0 || amount > 1) { throw new RangeError('amount must be between 0 and 1') } this.#speed = Math.max(0, this.#speed - amount * 8) } }
In this ElectricCar, speed and battery level are private fields. External code cannot change them directly and must use accelerate, which checks its input range, or brake, which prevents speed from falling below zero. Conditions the object must preserve can therefore be maintained in one place.
Encapsulation does not mean adding a getter and setter for every field. Exposing internal data directly makes callers depend on its structure. Publish operations that express intent, such as accelerate, instead of exposing raw assignments such as set speed.
An interface decides what to show outside; encapsulation decides how to hide and protect what is inside. They are distinct concepts, but they work together as two sides of the same boundary.
Polymorphism: same operation, different machinery
Polymorphism is the ability to treat multiple implementations of one interface through the same calls. Calling code programs against the shared promise instead of a concrete kind of object.
Driving code
CarControls
The same calls
GasolineCar: implemented by an engine
ElectricCar: implemented by a motor
In a gasoline car, the accelerator controls fuel and the engine; in an electric car, it controls power from the battery to a motor. Braking may use hydraulics or regeneration. If both vehicles fulfill CarControls, however, the driver-facing code can remain the same.
class GasolineCar implements CarControls { steer(direction: number) { /* Turn the front wheels */ } accelerate(amount: number) { /* Feed fuel and raise engine speed */ } brake(amount: number) { /* Apply hydraulic brakes */ } } class ElectricCar implements CarControls { steer(direction: number) { /* Steer with an electric motor */ } accelerate(amount: number) { /* Send power to the drive motor */ } brake(amount: number) { /* Use regenerative braking */ } } function startDriving(car: CarControls) { car.accelerate(0.4) car.steer(0.2) car.brake(0.3) } startDriving(new GasolineCar()) startDriving(new ElectricCar()) // The caller stays the same
The caller does not need an if or switch for every vehicle type. The object it receives performs its own implementation. A new vehicle can be added without changing startDriving as long as the shared promise remains intact.
Why this makes design easier
A small, explicit boundary lets users and implementers reason separately. Internals can change without forcing changes on the other side as long as the promise is preserved.
Creating an interface alone does not make a good design. Ambiguous names, huge lists of operations, and promises that implementations cannot keep create more coupling. Define the smallest set of operations users actually need, with clear meaning and constraints.
Replace implementations
Swap a gasoline car for an electric one, or real hardware for a simulator, while keeping the calling side when both fulfill the same promise.
Divide the work
Once a boundary is agreed, the calling and implementing sides can be built separately, and each team can limit what it needs to understand.
Test in isolation
Pass a fake with the same interface instead of a real car or television and verify the caller's behavior independently.
Common misconceptions
An interface is not only a screen. APIs, function inputs and outputs, public class operations, and signals between devices are interfaces between components.
Encapsulation is not merely adding private or generating getters and setters. It protects internal state and keeps users from depending on internal structure.
Polymorphism is not another name for an if or switch that inspects a type. Its core is calling through a shared promise and letting the concrete implementation choose its behavior.
Summary
An interface is a promise about operations available at a boundary. It states what can be done and separates users from how it is implemented.
Encapsulation locks implementation and state inside and preserves valid state through published operations. It is distinct from an interface but forms the other side of the same boundary.
Polymorphism lets different implementations of one interface be used by the same code, reducing type-specific branches in callers.
A small, clear promise makes replacement, division of work, and testing easier. Conforming to a type still does not guarantee that an implementation is correct.