ZetCode

TypeScript Interfaces

last modified October 15, 2026

An interface describes the shape of an object: which properties it has and what their types are. It is a named contract that objects must fulfil. Interfaces exist only at compile time; they produce no JavaScript code. The compiler uses them to check object literals, function arguments, and classes, and editors use them for autocompletion. Whether interface or type should be used for an object shape is discussed in the type keyword tutorial.

Basic Interface

An interface lists properties with their types. Objects of that type must have all of them.

basic_interface.ts
interface Person {
    name: string;
    age: number;
}

function introduce(person: Person): string {
    return `${person.name} is ${person.age} years old`;
}

const user: Person = { name: "Alice", age: 30 };

console.log(user);
console.log(introduce(user));
console.log(introduce({ name: "Bob", age: 25 }));

The Person interface is used to annotate a variable and a function parameter. Inside introduce, the compiler knows that person.name is a string and person.age a number. The function accepts any object that matches the interface, including an object literal written directly in the call.

$ npx tsx basic_interface.ts
{ name: 'Alice', age: 30 }
Alice is 30 years old
Bob is 25 years old

Checking Object Shapes

The compiler reports objects that do not match the interface.

interface_errors.ts
interface Person {
    name: string;
    age: number;
}

const a: Person = { name: "Alice" };
const b: Person = { name: "Bob", age: 25, email: "bob@example.com" };
const c: Person = { name: "Carol", age: "thirty" };

The first object is missing age, the third has a value of the wrong type. The second object has an extra property. Object literals are checked for excess properties, because an unknown property is usually a typo; the message even names the property that does not belong there.

$ npx tsc --noEmit
interface_errors.ts(6,7): error TS2741: Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.
interface_errors.ts(7,43): error TS2353: Object literal may only specify known properties, and 'email' does not exist in type 'Person'.
interface_errors.ts(8,36): error TS2322: Type 'string' is not assignable to type 'number'.

Optional Properties

A question mark after the property name makes the property optional.

optional_properties.ts
interface Person {
    name: string;
    age?: number;
}

function describe(person: Person): string {
    if (person.age === undefined) {
        return `${person.name} (age unknown)`;
    }

    return `${person.name} (${person.age})`;
}

const alice: Person = { name: "Alice" };
const bob: Person = { name: "Bob", age: 25 };

console.log(describe(alice));
console.log(describe(bob));

Inside the function, the type of person.age is number | undefined. The compiler requires a check before the value is used as a number. After the if statement, it knows that the age is defined.

$ npx tsx optional_properties.ts
Alice (age unknown)
Bob (25)

Readonly Properties

A readonly property can be set when the object is created, but not changed afterwards.

readonly_properties.ts
interface Person {
    readonly id: number;
    name: string;
}

const user: Person = { id: 1, name: "Alice" };

user.name = "Alicia";
user.id = 2;

Changing name is allowed, changing id is not. The check exists only at compile time; for runtime protection, Object.freeze is needed. The Readonly<T> utility type makes all properties of an interface read-only at once.

$ npx tsc --noEmit
readonly_properties.ts(9,6): error TS2540: Cannot assign to 'id' because it is a read-only property.

Methods

Interfaces can describe methods, either with method syntax or as properties of a function type.

methods.ts
interface Shape {
    name: string;
    area(): number;
    scale: (factor: number) => Shape;
}

function circle(radius: number): Shape {
    return {
        name: "circle",
        area: () => Math.PI * radius ** 2,
        scale: factor => circle(radius * factor),
    };
}

const small = circle(1);
const big = small.scale(3);

console.log(small.name, small.area().toFixed(2));
console.log(big.name, big.area().toFixed(2));

area(): number is method syntax and scale: (factor: number) => Shape describes a property that holds a function; for most purposes they are equivalent. The circle function returns objects that implement the interface, and the compiler infers the parameter factor as a number from the interface.

$ npx tsx methods.ts
circle 3.14
circle 28.27

Function Types

An interface with a call signature describes a function.

function_types.ts
interface Validator {
    (value: string): boolean;
    description: string;
}

function createValidator(description: string, test: (value: string) => boolean): Validator {
    const validator = (value: string) => test(value);
    return Object.assign(validator, { description });
}

const notEmpty = createValidator("must not be empty", v => v.trim().length > 0);
const isNumeric = createValidator("must be a number", v => /^\d+$/.test(v));

for (const v of [notEmpty, isNumeric]) {
    console.log(`${v.description}: ${v("42")}, ${v("  ")}`);
}

The call signature (value: string): boolean makes a Validator callable. Unlike a plain function type, the interface can also declare properties, here a description. Object.assign attaches the property to the function, and the result is both callable and has a description.

$ npx tsx function_types.ts
must not be empty: true, false
must be a number: true, false

Extending Interfaces

An interface can extend one or more other interfaces and add its own properties.

extending_interfaces.ts
interface Person {
    name: string;
    age: number;
}

interface Contact {
    email: string;
}

interface Employee extends Person, Contact {
    employeeId: number;
}

const employee: Employee = {
    name: "Alice",
    age: 30,
    email: "alice@example.com",
    employeeId: 12345,
};

function sendMail(to: Contact): string {
    return `mail sent to ${to.email}`;
}

console.log(employee);
console.log(sendMail(employee));

Employee has all properties of Person and Contact plus its own employeeId. An employee can be passed to any function that expects a Person or a Contact, because it has everything those interfaces require.

$ npx tsx extending_interfaces.ts
{
  name: 'Alice',
  age: 30,
  email: 'alice@example.com',
  employeeId: 12345
}
mail sent to alice@example.com

Index Signatures

An index signature describes objects whose property names are not known in advance, such as dictionaries.

indexable_types.ts
interface Scores {
    [subject: string]: number;
}

interface Config {
    name: string;
    [setting: string]: string | number;
}

const scores: Scores = { math: 92, physics: 85 };
scores.chemistry = 78;

const config: Config = { name: "app", port: 8080, mode: "dev" };

for (const [subject, score] of Object.entries(scores)) {
    console.log(`${subject}: ${score}`);
}

console.log(config.name, config.port);

[subject: string]: number means that any string key maps to a number, so new subjects can be added. An index signature can be combined with known properties, as in Config; the known properties must then fit the index signature, which is why it allows both string and number. Reading an unknown key such as scores.biology is typed as number, although it returns undefined; the noUncheckedIndexedAccess option makes such reads include undefined.

$ npx tsx indexable_types.ts
math: 92
physics: 85
chemistry: 78
app 8080

Generic Interfaces

Interfaces can have type parameters, so the same structure can be used with different types.

generic_interface.ts
interface ApiResponse<T> {
    status: number;
    data: T;
}

interface User {
    id: number;
    name: string;
}

const userResponse: ApiResponse<User> = {
    status: 200,
    data: { id: 1, name: "Alice" },
};

const listResponse: ApiResponse<string[]> = {
    status: 200,
    data: ["red", "green"],
};

console.log(userResponse.data.name);
console.log(listResponse.data.join(", "));

ApiResponse<User> and ApiResponse<string[]> share the status property, but their data has different types. The compiler knows that userResponse.data has a name and that listResponse.data is an array.

$ npx tsx generic_interface.ts
Alice
red, green

Implementing Interfaces

A class declares with implements that it fulfils an interface.

implements.ts
interface Logger {
    log(message: string): void;
    readonly prefix: string;
}

class ConsoleLogger implements Logger {
    constructor(readonly prefix: string) {}

    log(message: string): void {
        console.log(`${this.prefix} ${message}`);
    }
}

function runTask(logger: Logger): void {
    logger.log("task started");
    logger.log("task finished");
}

runTask(new ConsoleLogger("[app]"));

If ConsoleLogger lacked the log method or the prefix property, the class would not compile. The runTask function depends only on the interface, so any logger that implements it can be passed, for example one that writes to a file in production and one that collects messages in tests.

$ npx tsx implements.ts
[app] task started
[app] task finished

Structural Typing

TypeScript checks interfaces by structure. An object does not have to declare that it implements an interface; it only needs the right properties.

structural.ts
interface Point {
    x: number;
    y: number;
}

function distance(p: Point): number {
    return Math.hypot(p.x, p.y);
}

const location3d = { x: 3, y: 4, z: 12 };
console.log(distance(location3d));

class Vector {
    constructor(public x: number, public y: number) {}
}

console.log(distance(new Vector(6, 8)));

The location3d object has an extra z property, but it also has x and y, so it is a valid Point. The excess property check shown earlier applies only to object literals written directly where the type is expected, not to existing variables. The Vector class never mentions Point, but its instances match it as well.

$ npx tsx structural.ts
5
10

Declaration Merging

Interfaces with the same name are merged. This is used to add properties to existing interfaces.

merging.ts
interface Settings {
    theme: string;
}

interface Settings {
    language: string;
}

const settings: Settings = { theme: "dark", language: "sk" };
console.log(settings);

Both declarations contribute to one Settings interface. Libraries use this to let applications extend their types, for example to declare custom properties on a request object. See the declaration merging tutorial.

$ npx tsx merging.ts
{ theme: 'dark', language: 'sk' }

Best Practices

Source

TypeScript Handbook: Object Types

This tutorial covered interfaces in TypeScript. They give names to object shapes and let the compiler check that objects, functions, and classes fit them.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.