A TypeScript interface is a named description of an object’s shape, which lists the properties of the object, the type of each property and the signatures of its methods. The compiler checks every object, parameter and return value typed with the interface, and reports a missing property or a wrong type before the code runs. After compilation, the interface disappears, and the JavaScript output contains no trace of it.
In Java, an interface mostly lists the methods that a class must implement. In TypeScript, we also use interfaces for plain data such as API responses and configuration objects. An object matches an interface when it has the right properties, even if it never mentions the interface by name, which is called structural typing.
The following example declares a User interface, uses it as the type of an object, implements a Shape interface in a class and checks an object literal with satisfies. It uses TypeScript 7.0.2 in strict mode and Node.js 22.
// 1. Declare an interface
interface User {
readonly id: number; // cannot be reassigned
name: string;
age: number;
email?: string; // optional
greet(): string; // method signature
}
// 2. Use it as the type of an object
const lokesh: User = {
id: 1,
name: "Lokesh",
age: 37,
greet() { return "Hi, I am " + this.name; },
};
const message = lokesh.greet(); // message = "Hi, I am Lokesh"
const email = lokesh.email; // email = undefined
// 3. Implement an interface in a class
interface Shape {
area(): number;
}
class Square implements Shape {
side: number;
constructor(side: number) { this.side = side; }
area(): number { return this.side * this.side; }
}
const area = new Square(4).area(); // area = 16
// 4. Check an object literal with satisfies
const raj = { id: 2, name: "Raj", age: 35, greet: () => "Hi" } satisfies User;
const rajAge = raj.age; // rajAge = 35
Notice that the object lokesh leaves out the optional email property, so reading it returns undefined instead of a compile error.
Next, we look at optional and readonly properties, methods, index signatures and generic interfaces. After that, we implement interfaces in classes and compare an interface with a type alias.
1. Declaring an Interface and Using It as a Type
To declare an interface, we write the interface keyword, a name in PascalCase and a body in curly braces, where each line is a property name, a colon and a type. Semicolons or commas between members both work, but semicolons are the common style.
Once declared, the interface name works like any other type, so we can put it on a variable, a function parameter, a return value or an array.
interface User {
name: string;
age: number;
}
// 1. Type of a variable
const lokesh: User = { name: "Lokesh", age: 37 };
// 2. Type of a parameter and a return value
function describe(user: User): string {
return user.name + " is " + user.age;
}
const text = describe(lokesh); // text = "Lokesh is 37"
// 3. Type of an array
const users: User[] = [lokesh, { name: "Raj", age: 35 }];
const names = users.map((u) => u.name); // names = ["Lokesh", "Raj"]
The compiler checks object literals in two directions.
- Every required property must be present.
- An object literal assigned to a variable of the interface type may not add properties the interface does not declare.
The second rule is called an excess property check, and it catches typos such as nmae instead of name. For example, a profile page renders a user from a test fixture, and without the check, a misspelled nmae key would show up as an empty name on the screen.
const john: User = { name: "John" };
const raj: User = { name: "Raj", age: 35, city: "Pune" };
error TS2741: Property 'age' is missing in type '{ name: string; }' but required in type 'User'.
error TS2353: Object literal may only specify known properties, and 'city' does not exist in type 'User'.
The excess property check applies only to object literals. If an object with an extra city property is first stored in a variable and then assigned to User, the compiler accepts it, because the object still has every property a User needs. The same rule applies to the other ways of creating objects from an interface.
2. Optional and readonly Properties
A question mark after the property name makes the property optional, so the object may leave it out, and reading it returns undefined. With strict mode on, the type of email?: string is string | undefined, so the compiler makes us handle the missing case before we use the value as a string.
The readonly modifier allows a property to be set when the object is created and blocks any later assignment, much like a final field in Java.
interface User {
readonly id: number;
name: string;
age: number;
email?: string;
}
const john: User = { id: 3, name: "John", age: 40 };
// 1. An optional property may be missing
const email = john.email; // email = undefined
const shown = john.email ?? "no email"; // shown = "no email"
// 2. A readonly property can be read, not reassigned
const id = john.id; // id = 3
// 3. Other properties can change
john.age = 41; // age = 41
Assigning a new value to john.id fails at compile time with error TS2540.
error TS2540: Cannot assign to 'id' because it is a read-only property.
Two limits of readonly matter in practice.
- The readonly check happens at compile time only, so nothing stops plain JavaScript code from changing the value at runtime.
- The check is shallow, so a readonly property that holds an object or an array blocks reassignment of the property, but the contents of the object can still change.
For a deeply immutable value, we combine readonly with as const, ReadonlyArray or Object.freeze().
3. Methods and Function Types in an Interface
An interface can also declare methods. For each method, we write only the signature, which is the method name with its parameter and return types, and no body. TypeScript accepts two syntaxes for a method.
- The method syntax looks like greet(): string.
- The property syntax looks like isOlderThan: (age: number) => boolean.
Both describe a function that the object must provide.
interface User {
name: string;
age: number;
greet(): string; // method syntax
isOlderThan: (age: number) => boolean; // property syntax
}
const raj: User = {
name: "Raj",
age: 35,
greet() { return "Hi, I am " + this.name; },
isOlderThan(age) { return this.age > age; },
};
const hello = raj.greet(); // hello = "Hi, I am Raj"
const older = raj.isOlderThan(30); // older = true
The object literal does not repeat the parameter type of isOlderThan(age), because TypeScript takes it from the interface and types age as number. Inside the methods, this has the type User, which is why this.name compiles.
An interface can also describe a function itself. For that, we write a call signature (a parameter list and return type without a name), and any function with matching parameters and return type can be assigned to it.
interface Formatter {
(user: User): string;
}
const shortName: Formatter = (user) => user.name.toUpperCase();
const upper = shortName(raj); // upper = "RAJ"
For a single function, a type alias such as type Formatter = (user: User) => string is shorter and more common. The interface form is useful when the function also has properties.
4. Index Signatures for Dictionary Objects
Some objects do not have a fixed list of property names, for example a lookup from a person’s name to an age, where we do not know the names when we write the code. An index signature describes a dictionary object by the type of its keys and the type of its values.
interface Ages {
[name: string]: number;
}
const ages: Ages = { Lokesh: 37, Raj: 35 };
ages["John"] = 40; // any string key is allowed
const count = Object.keys(ages).length; // count = 3
const brian = ages["Brian"]; // brian = undefined, typed as number
The last line shows a gap in the default checks. The compiler types ages[“Brian”] as number, but at runtime the key does not exist and the value is undefined. The compiler option noUncheckedIndexedAccess changes the type to number | undefined and forces a check. The built-in Record<string, number> type describes the same object in one line, and a TypeScript Map fits better when keys change often at runtime.
5. Nested and Generic Interfaces
A property type can be another interface, so we can describe nested data one level at a time. Here a User contains an Address, and the compiler checks the inner object with the same rules as the outer one.
A generic interface takes a type parameter in angle brackets, so one declaration works for many value types. Box<number> holds a number and Box<User> holds a user. The built-in types Array<T>, Set<T>, Map<K, V> and Promise<T> are declared this way.
// 1. An interface as a property type
interface Address {
city: string;
}
interface User {
name: string;
age: number;
address: Address;
}
const lokesh: User = { name: "Lokesh", age: 37, address: { city: "Delhi" } };
const city = lokesh.address.city; // city = "Delhi"
// 2. A generic interface
interface Box<T> {
value: T;
}
const ageBox: Box<number> = { value: 37 };
const userBox: Box<User> = { value: lokesh };
const boxedName = userBox.value.name; // boxedName = "Lokesh"
A generic interface can also restrict its type parameter with extends or give it a default, as we do with other TypeScript generics.
6. Implementing an Interface in a Class
As in Java, a class declares that it follows an interface with the implements keyword, and it can implement several interfaces separated by commas. The compiler checks that the class has every required property and method with compatible types. For example, a checkout page can accept any class that implements a PaymentMethod interface, so adding a new payment provider needs no change in the page code.
interface Shape {
name: string;
area(): number;
}
interface Printable {
print(): string;
}
// 1. One class, two interfaces
class Rectangle implements Shape, Printable {
name = "rectangle";
width: number;
height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
area(): number {
return this.width * this.height;
}
print(): string {
return this.name + " " + this.area();
}
}
// 2. Use the class through the interface type
const shape: Shape = new Rectangle(3, 4);
const area = shape.area(); // area = 12
const printed = new Rectangle(2, 5).print(); // printed = "rectangle 10"
When a method is missing, the error names the class, the interface and the member. For a Circle class that implements Shape and declares name but no area() method, the compiler reports error TS2420.
error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
Property 'area' is missing in type 'Circle' but required in type 'Shape'.
The implements clause is only a check. It does not copy anything into the class, and the parameter types of the class methods are not taken from the interface, so we still write them. Because TypeScript compares shapes, a class that has area() and name can be used as a Shape even without the implements clause. We still write it, because it documents the intent and reports a missing method at the class instead of at some distant call site. An interface and a class also differ at runtime, because only the class stays in the JavaScript output.
7. Checking an Object Literal With satisfies
The satisfies operator, added in TypeScript 4.9, checks a value against a type without changing the type of the variable. With an annotation such as const raj: User, the variable gets the type User. With satisfies User, the compiler still reports missing or extra properties, but the variable keeps the more precise type of the literal.
The difference shows up when the interface uses a union type, such as an age that can be a number or a string.
interface User {
name: string;
age: number | string;
}
// 1. Type annotation: the variable has type User
const raj: User = { name: "Raj", age: 35 };
// raj.age is number | string
// 2. satisfies: checked against User, keeps the literal's own type
const john = { name: "John", age: 40 } satisfies User;
const nextAge = john.age + 1; // nextAge = 41, age is number
Writing raj.age + 1 fails with error TS2365: Operator ‘+’ cannot be applied to types ‘string | number’ and ‘number’, because the annotation widened age to the union. We use an annotation when the variable must accept any User later, and satisfies for constant objects such as configuration, where we want both the check and the exact types.
8. Interface vs Type Alias
A type alias gives a name to any type with the type keyword. For object shapes, an interface and a type alias are almost interchangeable. Both can be used as a type or implemented by a class, and a value of one is assignable to the other when the shapes match.
// 1. Both describe an object shape
interface UserI {
name: string;
age: number;
}
type UserT = {
name: string;
age: number;
};
const a: UserI = { name: "Lokesh", age: 37 };
const b: UserT = a; // same shape, so assignable
// 2. Only a type alias can name a union, a tuple or a primitive
type Id = number | string;
type Pair = [string, number];
// 3. Only an interface merges with a second declaration
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
const settings: Settings = { theme: "dark", fontSize: 14 }; // needs both
Two interface Settings declarations in the same scope merge into one interface with both properties. TypeScript calls this declaration merging, and libraries use it to let us add properties to their types. For example, an Express app that sets the logged-in user in a middleware adds a user property to the Request interface of Express. Two type declarations with the same name are an error (TS2300: Duplicate identifier).
In day-to-day code, the differences that matter are union types and declaration merging.
| Feature | interface | type alias |
|---|---|---|
| Object shape with properties and methods | Yes | Yes |
| Union, tuple or primitive types | No | Yes, for example type Id = number | string |
| Reuse another type | extends | Intersection with & |
| Declaration merging | Yes | No, a second declaration is an error |
| Implemented by a class | Yes | Yes, when the alias is an object type |
The TypeScript Handbook suggests using an interface until we need a feature that only a type alias has. Most teams follow that rule, so they use interfaces for object shapes and type aliases for unions, tuples, function types and mapped types. An interface reuses another one with the extends keyword, and a type alias does the same with an intersection.
9. Building and Running the Interface Examples
The TypeScript-Examples repository on GitHub holds a runnable project with one file per section in src, and index.ts runs the files in order. Building it requires Node.js 22 or a later release; package.json pins TypeScript to 7.0.2.
npm install
npm start
The npm start script compiles the code and prints every value shown in the snippet comments, one block per section. Opening a compiled file in dist/src confirms that the interfaces are gone and only plain objects and classes remain.
10. Conclusion
A TypeScript interface names the shape of an object, and the compiler uses it to check object literals, function parameters, return values and classes. Optional and readonly properties, method signatures, index signatures and generic parameters cover most real data. Since interfaces are erased during compilation, they cost nothing at runtime, but they also cannot be checked with instanceof, so we check the properties of the object instead. For object shapes, an interface is the usual choice; for unions and tuples, a type alias is.
11. References
The TypeScript Handbook and the TSConfig reference give the full rules for each feature we used.
- TypeScript Handbook: Object Types
- TypeScript Handbook: Differences Between Type Aliases and Interfaces
- TypeScript Handbook: Declaration Merging
- TypeScript 4.9 release notes: the satisfies operator
- TSConfig reference: noUncheckedIndexedAccess
Happy Learning !!