An interface gives a name to the shape of an object: the properties it must have and the type of each. Once declared, you use the name as a type, and the compiler checks every object you pass, return or assign against it.
The last call is compile error TS2741. Interfaces are erased when the code is compiled: the JavaScript output has no trace of User, and nothing checks the shape at runtime.
Declaring an Interface
The syntax is the keyword interface, a name (PascalCase by convention), and a body listing members. Members can be separated by semicolons, commas or just line breaks; semicolons are the common style.
interface Product {
sku: string; // required property
price: number;
tags: string[]; // array property
dimensions: { // nested object type
width: number;
height: number;
};
discount?: number; // optional property
readonly createdAt: Date; // cannot be reassigned
label(): string; // method
}
An interface is a type, not a value. It cannot be instantiated with new, it has no default values, and obj instanceof Product is error TS2693 ('Product' only refers to a type, but is being used as a value here). To check a shape at runtime, write a type guard.
Structural Typing and Excess Property Checks
TypeScript compares shapes, not names. Any object with the required properties fits the interface, whether or not it was declared with it. Extra properties are fine, with one exception: an object literal written directly where the interface is expected gets an excess property check, because an unknown key there is almost always a typo.
That error (TS2353) is what catches { id: 1, name: "a", emial: "x" } for a User: the compiler even suggests Did you mean to write 'email'? (TS2561).
Optional and Readonly Properties
A ? after the name makes a property optional: the object may leave it out, and reading it gives T | undefined. readonly forbids reassigning the property after the object is created.
Two limits show here. First, readonly is compile-time only: the two lines marked @ts-expect-error still run when you press Run, and they succeed, and the last lines change apiUrl through a reference typed without readonly. Second, it is shallow: readonly hosts: string[] would stop reassigning hosts but still allow hosts.push(...), which is why the array itself is typed readonly string[]. It documents and enforces intent in typed code; it does not freeze anything. The Readonly<T> utility type makes every property of an existing interface readonly at once.
Methods and Function Properties
A method can be written as a method signature, name(params): ReturnType, or as a property holding a function, name: (params) => ReturnType. Callers use both the same way.
The difference is subtle: under strictFunctionTypes (part of strict), parameters of function-typed properties are checked strictly, while method signatures are checked more loosely (bivariantly), so the property form catches a few more mistakes. Method syntax is shorter and the more common style; both are fine.
An interface can also describe something callable or constructible, using a call signature or a construct signature:
interface Formatter {
(value: number): string; // call signature: the object is a function
locale: string; // and it also has a property
}
interface PointConstructor {
new (x: number, y: number): { x: number; y: number }; // construct signature
}
Index Signatures
When the property names are not known in advance, an index signature describes all of them at once: [key: string]: T means "any string key, each holding a T".
The last lines show the catch: reading a key that does not exist is typed as number, not number | undefined. The compiler option noUncheckedIndexedAccess adds | undefined to every such read.
Named properties can sit next to an index signature, but they must fit it. interface Dict { [key: string]: number; name: string } is error TS2411, Property 'name' of type 'string' is not assignable to 'string' index type 'number'. Widen the index type ([key: string]: number | string) or move the dynamic part into its own property. For simple key/value maps, Record<string, number> says the same thing in one line.
Extending an Interface
extends builds a new interface from one or more existing ones. The child has every parent member plus its own:
interface Animal {
name: string;
}
interface Pet extends Animal {
owner: string;
}
interface Trained {
commands: string[];
}
interface ServiceDog extends Pet, Trained {
certifiedUntil: Date;
}
// ServiceDog requires: name, owner, commands, certifiedUntil
A child may redeclare a parent property only with a compatible (narrower) type, such as kind: "dog" where the parent says kind: string. The rules, and how to extend type aliases, are on the extends page.
Implementing an Interface in a Class
class X implements Shape asks the compiler to check that the class has everything the interface requires. A missing member is an error at the class declaration:
index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
Property 'area' is missing in type 'Circle' but required in type 'Shape'.
With area() added, several classes and even a plain object can all be used as a Shape:
implements is only a check. It does not add members to the class, and it does not type the class's method parameters for you: greet(name) {} inside a class that implements greet(name: string): string is still error TS7006, Parameter 'name' implicitly has an 'any' type. A class can implement several interfaces: class A implements B, C.
Declaration Merging
Declaring an interface with the same name twice in the same scope merges the two into one. This is something type aliases cannot do (a second type with the same name is a duplicate identifier error).
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };
In application code this is rarely what you want, and an accidental merge can be confusing. Its real use is adding members to types you do not own: a library's options, or a global such as Window. From inside a module, wrap the declaration in declare global:
declare global {
interface Window {
analytics: { track(event: string): void };
}
}
export {};
After this, window.analytics.track("signup") type-checks everywhere in the project. Type definition packages rely on the same mechanism; see declaration files.
Default Values for Interface Properties
An interface cannot hold default values, because it describes types and is erased at runtime. size?: "sm" | "md" = "md" is error TS1246, An interface property cannot have an initializer. Make the property optional and fill in the default where the object is used:
Destructuring defaults are the safer choice: they apply whenever the value is undefined, including an explicit size: undefined. The spread version copies that explicit undefined over the default, and its result is still typed as if size were always set. If you need that guarantee from the types, turn on exactOptionalPropertyTypes, which makes size: undefined a compile error for an optional size?: .... A class with initialized fields is the other option when the object needs behavior too.
Generic Interfaces
An interface can take type parameters, which makes one declaration work for many payload types:
interface ApiResponse<T> {
ok: boolean;
data: T;
error?: string;
}
interface Page<T> {
items: T[];
nextCursor?: string;
}
interface User {
id: number;
name: string;
}
const res: ApiResponse<Page<User>> = {
ok: true,
data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};
ApiResponse<Page<User>> reads as "a response whose data is a page of users". The standard library is full of these: Array<T>, Promise<T>, Map<K, V> are all generic interfaces.
Interface vs Type Alias
A type alias can describe the same object shape, and for plain object types the two are interchangeable. Only an interface can merge; only a type alias can name a union, a tuple, or a mapped or conditional type. The TypeScript handbook's rule of thumb is to use interface until you need a feature that only type has. The interface vs type page has the full comparison, including the Record<string, ...> difference that surprises most people.
Frequently Asked Questions
What is an interface in TypeScript?
An interface is a named description of an object's shape: its property names, their types, which ones are optional or readonly, and its methods. The compiler checks that values used as that interface have that shape. Interfaces exist only at compile time; they produce no JavaScript.
How do I set a default value in a TypeScript interface?
You cannot: an interface describes types, not values, so size: "md" = ... is not valid syntax. Mark the property optional (size?: "sm" | "md") and apply the default where the object is used, usually with destructuring defaults in the function parameters: function render({ size = "md" }: Options). Spreading a defaults object ({ ...DEFAULTS, ...options }) also works, but an explicit undefined in options overwrites the default.
How do I check if an object implements an interface at runtime?
There is no built-in way, because interfaces are erased during compilation: obj instanceof User is error TS2693 ('User' only refers to a type, but is being used as a value here). Write a type guard function that checks the properties, function isUser(x: unknown): x is User { ... }, or validate with a schema library.
Can an interface extend multiple interfaces?
Yes. List them after extends, separated by commas: interface ServiceDog extends Pet, Trained { ... }. The new interface has every member of each parent plus its own. If two parents declare the same property with incompatible types, the declaration is an error.
What is the difference between an interface and a class in TypeScript?
A class exists at runtime: it has a constructor, method implementations, and new creates objects from it. An interface only describes a shape for the compiler and is erased from the JavaScript output. A class can declare implements SomeInterface to have the compiler check that it matches, and any plain object with the right shape fits the interface too.