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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
- Name the Shapes of Your Data: Define interfaces for objects passed between functions and modules.
- Keep Interfaces Small: Several focused interfaces combined with
extendsare easier to reuse than one large interface. - Mark Optional and Readonly Properties: Make the contract precise, and handle
undefinedfor optional properties. - Depend on Interfaces: Accept interface types in functions, so that different implementations can be used.
- Be Careful with Index Signatures: They accept any key; prefer known properties or a
Mapwhen possible.
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
List all TypeScript tutorials.