TypeScript Interface: Declare, Implement and Use (Examples)

A TypeScript interface names the shape of an object. Learn to declare one, use optional and readonly properties, implement it in a class, and choose between an interface and a type alias.

A TypeScript interface is a named description of an object’s shape: which properties the object has, the type of each property, and the signatures of its methods. The compiler uses the interface to check every object, parameter and return value that claims to have that shape, and reports a missing property or a wrong type before the code runs. After compilation, the interface disappears; the JavaScript output contains no trace of it.

Java developers know the interface keyword, but a TypeScript interface is used in more places. In Java, an interface mostly lists methods that a class must implement. In TypeScript, we also use interfaces for plain data such as API responses and configuration objects, and an object matches an interface when it has the right properties, even if it never mentions the interface by name. This is called structural typing.

This tutorial shows how to declare an interface, mark properties as optional or readonly, add methods, describe dictionaries with index signatures, and implement an interface in a class. It also covers the satisfies operator and the difference between an interface and a type alias. All examples were compiled with TypeScript 7.0.2 in strict mode and run on Node.js 22.

A single snippet covers the syntax that most code needs. The numbered sections explain each part and the compiler errors that go with it.

// 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

1. Declaring an Interface and Using It as a Type

We write the interface keyword, a name in PascalCase, and a body in curly braces. Each line in the body is a property name, a colon and a type. Semicolons or commas between members both work; semicolons are the common style.

Once declared, the interface name works like any other type. 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 written directly against the interface 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.

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 article Creating Objects from Interface in TypeScript shows this case and the other ways to build objects that match an interface.

2. Optional and readonly Properties

A question mark after the property name makes the property optional. 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. Java developers can compare it to a final field.

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 to john.id fails at compile time:

error TS2540: Cannot assign to 'id' because it is a read-only property.

Two limits of readonly matter in practice:

  • It is a compile-time check only, so nothing stops plain JavaScript code from changing the value at runtime.
  • It is shallow: a readonly property that holds an object or an array blocks reassignment of the property, but the contents of that object can still change.

For a deeply immutable value, we combine it with as const, ReadonlyArray or Object.freeze().

3. Methods and Function Types in an Interface

An interface can declare methods. We write only the signature: the name, the parameters and the return type, with no body. TypeScript accepts two syntaxes for a method:

  • the method syntax greet(): string
  • the property syntax 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). TypeScript takes it from the interface, so age is a number. Inside the methods, this has the type User, which is why this.name compiles.

An interface can also describe a function itself. We write a call signature, a parameter list and return type without a name. 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. A lookup from a person’s name to an age is one example: we do not know the names when we write the code. An index signature describes such an 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; see TypeScript Record for when to prefer it, and TypeScript Map for keys that 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. 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>, 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"

Generic interfaces with constraints and default type parameters are covered in TypeScript Generics.

6. Implementing an Interface in a Class

A class declares that it follows an interface with the implements keyword, as in Java. A class can implement several interfaces, separated by commas. The compiler then checks that the class has every required property and method with compatible types.

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: 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. The differences between the two constructs are listed in TypeScript Interface vs Class.

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 exactly 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. In the example below, age 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, both can be 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. This is called declaration merging, and libraries use it to let us add properties to their types. Two type declarations with the same name are an error (TS2300: Duplicate identifier). The differences that affect day-to-day code are summarized in this table.

Featureinterfacetype alias
Object shape with properties and methodsYesYes
Union, tuple or primitive typesNoYes, for example type Id = number | string
Reuse another typeextendsIntersection with &
Declaration mergingYesNo, a second declaration is an error
Implemented by a classYesYes, 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: interfaces for object shapes, type aliases for unions, tuples and function types. Extending interfaces, merging and intersections are explained in TypeScript Extend Interface.

9. Building and Running the Interface Examples

Every snippet in this article is part of a runnable project in the TypeScript-Examples repository on GitHub. Each section has its own file in src, and index.ts runs them 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; TypeScript instanceof shows what to use instead. For object shapes, an interface is the usual choice; for unions and tuples, a type alias is.

11. References

The TypeScript Handbook pages below describe object types, the interface and type alias comparison, and the satisfies operator in detail.

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.