Chapter 5 of 24 · TypeScript for Beginners — Complete Free Tutorial
Interfaces and Type Aliases
Typing objects inline works until the same shape appears twice. This chapter of the TypeScript for Beginners series introduces the two naming tools — interface and type — plus optional and readonly members, inheritance with extends, a clear comparison of when each keyword wins, and structural typing, the rule that lets TypeScript accept objects you never explicitly connected to a contract.
Declaring an interface
An interface is a named contract describing an object's properties. Declare it once, annotate with it everywhere, and let the compiler enforce the details at every use site:
interface User {
id: number;
name: string;
email: string;
}
const ada: User = { id: 1, name: "Ada", email: "ada@example.com" };
console.log(ada.name.toUpperCase());
Expected output: ADA — and note that ada. now offers id, name and email in editor completion. Leave a member out and the compiler names it:
const grace: User = { id: 2, name: "Grace" };
Expected output:
error TS2739: Type '{ id: number; name: string; }' is missing properties from type 'User': 'email'.
Interfaces scale in a way inline literals never do: change the contract in one place and every file that relies on it is re-checked immediately.
Optional and readonly members
The modifiers you learned on object literals work identically inside an interface, which is where they become genuinely useful:
interface User {
id: number;
name: string;
email?: string; // may be omitted entirely
readonly createdAt: Date; // fixed once created
}
const ada: User = { id: 1, name: "Ada", createdAt: new Date() };
ada.createdAt = new Date();
Expected output:
error TS2540: Cannot assign to 'createdAt' because it is a read-only property.
Two nuances prevent surprise later. First, email? makes the property string | undefined, so reading it without a guard draws the same complaint any possibly-absent value does. Second, readonly is shallow: if a member is readonly tags: string[], you cannot point tags at a different array, but tags.push(...) still works. The array itself was never locked.
Extending interfaces
Interfaces compose with extends, so a general contract can specialise without copying a single member:
interface User {
id: number;
name: string;
}
interface Admin extends User {
permissions: string[];
}
const root: Admin = {
id: 1,
name: "Ada",
permissions: ["read", "write", "deploy"],
};
Expected output: no errors — root must satisfy User and Admin together. One interface may extend several at once (interface Auditor extends User, HasBadge {}), and if a child member contradicts its parent, the compiler reports Interface 'Admin' incorrectly extends interface 'User' rather than letting both stand.
Interface vs type alias
type can name a shape too, which confuses every beginner: type User = { id: number } behaves almost exactly like the interface above. The differences are real, though, and knowing them settles most style debates:
| Capability | interface | type |
|---|---|---|
| Object shapes | Yes | Yes |
| Unions, primitives, tuples | No — it can only describe objects | Yes — type ID = string | number |
| Declaration merging | Yes — two blocks combine | No — a redeclaration is Duplicate identifier |
| Extending | extends | extends too, or intersect with & |
| Mapped and conditional types | No | Yes — the machinery of chapter 13 |
| Best suited to | Object contracts, public APIs, library augmentation | Unions, helpers, anything not an object shape |
Declaration merging deserves a second look, because it is the interface feature with no type equivalent:
interface Product {
id: number;
name: string;
}
interface Product {
price: number;
}
// Product now requires id, name AND price
Two separate declarations combine into one contract — invaluable when you must extend a definition you do not own, such as a library's types or the DOM's window. Declare the same alias twice and you simply get error TS2300: Duplicate identifier 'Product'.
Structural typing: implicit implementation
TypeScript is structurally typed — often called duck typing: if a value has the members the contract requires, it satisfies the contract, regardless of its name, its source file or any implements clause. A function accepts anything shaped like Point:
interface Point {
x: number;
y: number;
}
function show(point: Point) {
console.log(`${point.x}, ${point.y}`);
}
const cursor = { x: 10, y: 20, label: "pointer" }; // extra field, from a variable
show(cursor);
show({ x: 10 });
Expected output:
10, 20
error TS2741: Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.
Notice the asymmetry: cursor passes with its extra label field, because values from variables are checked on structure alone. A fresh literal written directly at the call site is held to a stricter standard — show({ x: 10, y: 20, z: 30 }) is rejected with Object literal may only specify known properties, and 'z' does not exist in type 'Point'. The compiler allows extra data in general, but stops the typos it can still catch cheaply.
That is also what “implicit implementation” means: the object below never says implements Logger anywhere, yet it can be passed wherever a Logger is required, because its structure matches:
interface Logger {
log(message: string): void;
}
function save(logger: Logger, data: string) {
logger.log(data);
}
const consoleLogger = { log: (m: string) => console.log(m) };
save(consoleLogger, "record saved");
Expected output: record saved. Classes may still write implements Logger for clarity and for the documentation value — you will see that in chapter 12 — but the compiler never requires it. One related rule from objects in JavaScript carries over: interfaces and types are compile-time only, so there is no such thing as instanceof User once the code is running.
Common mistakes
- Expecting an interface at runtime — interfaces are erased during compilation; use a class or a value when you need something to check against later.
- Writing a union with
interface—interface Status = "on" | "off"is invalid; primitives and unions belong totype. - Redeclaring a
typehoping to merge — aliases cannot be redeclared; only interfaces merge, and only in compatible ways. - Assuming
readonlyfreezes everything — it blocks reassignment of that member, not mutation of the array or object inside it. - Declaring a shape five times inline — copies drift; name the contract once so one edit re-checks every use.
Practice checklist
- Define a
Productinterface with one required, one optional and one readonly member, and violate each in turn. - Extend
UserintoAdmin, then try to add a conflictingnametype and read the error. - Rewrite one interface as an equivalent
type, then attempt to merge a second declaration of each and compare the results. - Pass a variable with extra properties to a typed function, then pass an inline literal with the same extra property, and note the different outcomes.
- Build the
Loggerexample with an object literal that never mentions the interface, and confirm it compiles.
What's next?
Your data now has named, enforced contracts — the next step is typing the functions that receive and return it in Functions in TypeScript.
Frequently Asked Questions
TypeScript is a strict superset of JavaScript that adds a static type system. You annotate the promises your data makes, the compiler checks every one before the code runs, then erases the types and emits ordinary JavaScript. You catch wrong arguments, typoed properties and forgotten null checks while you are still typing, and refactoring plus editor autocomplete both improve sharply.
You should. This series assumes you can already read and write JavaScript: functions, arrays and objects, closures, modules and async/await. TypeScript audits that knowledge rather than replacing it, and types cannot rescue code you do not understand. If those fundamentals feel shaky, work through the JavaScript for Beginners series on this site first, and this course will go much faster.
No. TypeScript is compiled, not interpreted: tsc parses your file, type-checks it, then erases every annotation and writes ordinary JavaScript. The browser and Node.js only ever run that emitted .js, so runtime behaviour, performance and debugging are unchanged. It also never validates API data at runtime, so the series teaches you to distrust every boundary that leaves the compiler.
If you already know JavaScript, TypeScript is a modest step rather than a new language: the syntax is the same plus a colon. Most beginners read their first red squiggle within days and feel comfortable in two to four weeks of regular practice. The curve steepens later around generics and advanced types, which is where this 24-chapter series deliberately spends its second half.
Both name the same object shapes, so either works for a simple contract. The differences decide the rest: only interfaces merge when declared twice, while only type aliases can express unions, primitives, tuples and mapped types. The series default is interface for props and exported shapes other files consume, and type for everything else. Pick one style per project and stay consistent.
strict: true is a bundle of safety switches, not a single behaviour. It turns on noImplicitAny, so every parameter must be inferrable, strictNullChecks, so null and undefined are their own types you must narrow, strictFunctionTypes, noImplicitThis, strictPropertyInitialization, and useUnknownInCatchVariables, which types catch values as unknown. Every example in this series compiles with it on.
any accepts anything and switches the compiler off, so one any spreads through inference into everything derived from it and you are back to dynamic typing with a .ts extension. unknown accepts anything too, but forbids property access until you narrow it with a typeof or instanceof check. The series pushes you toward unknown because it keeps the check you installed TypeScript to get.
Yes to both. Vite understands TypeScript natively: scaffold the vanilla-ts or react-ts template and a tsconfig.json is waiting for you, with moduleResolution set for the bundler. JSX belongs in .tsx files, props get an interface, and hooks infer or accept explicit type arguments. The series covers typed React in depth, from props, state and events to refs, context and fetch effects.
Enums still compile and work fine in TypeScript 5.x, but they are no longer the default recommendation. A union of string literals such as "light" | "dark" needs no runtime object, matches JSON directly and plays well with single-file transpilers like Vite's esbuild. Keep enum for existing codebases that already use it, avoid const enum, and prefer literal unions in new code.
Adopt it incrementally instead of rewriting. Install typescript, add a tsconfig.json with allowJs and noEmit so .js and .ts coexist while your bundler keeps building, then check individual files with a // @ts-check comment. Rename leaf modules to .ts one at a time, fix implicit any errors first, and gate every pull request with tsc --noEmit so untyped code cannot sneak back in.