Interfaces and type aliases
Two ways to name a shape, and the one difference that decides which to use.
After this lesson you can
- Name an object shape with either form
- Say what an interface can do that a type alias cannot
- Mark a property optional or readonly and know what that guarantees
Both of these describe the same object:
interface User {
id: string;
email: string;
name?: string; // may be absent
readonly createdAt: Date;
}
type UserAlias = {
id: string;
email: string;
name?: string;
readonly createdAt: Date;
};
name? means the property may be missing, so its type is
string | undefined and you have to handle that. readonly stops
assignment after construction — at compile time only; nothing prevents it at
runtime.
The difference that matters
An interface is open: declaring it twice merges the declarations.
interface Window { myThing: string } // adds to the existing Window
(That line merges with the DOM's Window only where it is declared in
the global scope: a script file, or inside declare global in a module.)
A type alias is closed: declaring it twice is an error. It can, however, name things an interface cannot — a union, a tuple, a mapped or conditional type:
type Status = "draft" | "live"; // interface cannot
type Pair = [string, number]; // interface cannot
So: interface for an object shape others may need to extend, especially a
public API or a global. type for everything else. Beyond that the two are
interchangeable, and a codebase that picks one and sticks to it is easier to
read than one that agonises.
Try it
interface User { id: string; email: string; name?: string; readonly createdAt: string;} function display(u: User): string { // name may be undefined, so ?? supplies a real fallback. return `${u.name ?? u.email} (since ${u.createdAt})`;} export function run(): string[] { return [ display({ id: "1", email: "nino@example.com", createdAt: "2024-01-01" }), display({ id: "2", email: "ana@example.com", name: "Ana", createdAt: "2024-06-01" }), ];}Structural typing
TypeScript does not care what you called a type. If a value has the right shape, it fits:
function greet(u: { name: string }) { return `Hi ${u.name}`; }
const nino = { name: "Nino", extra: 1 };
greet(nino); // fine: it has a name, extra is ignored
greet({ name: "Nino", extra: 1 }); // error: excess property 'extra'
Passing an object literal directly is the exception: excess property
checking rejects extra there, on the grounds that you probably made a typo.
A later lesson shows what the extra property costs once it gets through.
readonly is a compile-time promise
readonly stops you assigning through that type. It does not freeze the
object, and a readonly object may be assigned to a type without readonly,
which can then write to it:
interface Invoice { readonly id: string; readonly total: number;} // A mutable view of the same shape. Assigning a readonly object to it is allowed.interface Draft { id: string; total: number;} export function run(): number[] { const invoice: Invoice = { id: "INV-1", total: 100 }; const before = invoice.total; const draft: Draft = invoice; // no error: readonly does not block this draft.total = 0; // and this writes to the very same object return [before, invoice.total];}When you need the object itself to refuse writes, that is Object.freeze,
a runtime call.
Common mistakes
- Interpolating an optional property. A template string accepts
undefinedwithout complaint, so removing the?? u.emailfrom the first example still compiles and prints"undefined (since 2024-01-01)". The optional type protects you only whereundefinedcannot be used, such as calling a method on it. - Expecting
readonlyto protect the object. As above: one assignment to a mutable alias and the "readonly" total is0. - Declaring an interface twice by accident. It does not fail; the two merge, and the shape now requires both sets of properties.
- Relying on excess property checks. They only apply to a fresh object literal. Any variable with the right properties gets through, extras and all.
What interviewers ask
interfaceortype— how do you choose? An interface can be merged and extended, which suits public object shapes; unions, tuples, mapped and conditional types needtype. Otherwise consistency matters more.- What does structural typing mean in practice? Compatibility is decided by shape, not by name: anything with the required properties fits.
- No. It is checked at compile time only and can be bypassed through a mutable alias; use (shallow) for runtime immutability.
Try it yourself
2 visible tests · 4 hidden testsImplement initials(user: { name?: string; email: string }): string.
When name is present, return one uppercase letter per word ("Nino Beridze" → "NB"). When it is absent, fall back to the first two
letters of the email's local part, uppercased ("ana@example.com" →
"AN").
initials({"name":"Nino Beridze","email":"nino@example.com"})initials({"email":"ana@example.com"})
Sign up to run the tests, check the hidden ones and save your progress. Sign up