Made by VeraStack Labs
Contents
A form with boxes
An object has a shape. A type writes it down.
Picture a form at a clinic desk. It has boxes: Name, Age, Phone. The desk clerk doesn't accept a form with the Age box empty. Some boxes are marked "optional", like Email. And a few are printed in ink, not pencil: your patient number can't be changed once it is written.
A JavaScript object is that form, filled in: { name: "Asha", age: 21 }. The trouble is that JavaScript has no printed form. Nothing says which boxes must be filled, which are optional, or which are in ink. Any code can add a box, forget a box, or rub one out.
TypeScript lets you print the form. You write the shape of the object once: which properties it has, and what kind of value each one holds. Then every object that claims to be one of these is checked against it.
This lesson is the one you will use the most. Almost every real program is a pile of objects.
Remember: a type for an object is a printed form: the boxes, their kinds, which are optional, and which are in ink.
A form with no form
Missing, misspelled and surprise fields all pass.
This function wants a user with a name and an age.
function describeUser(user) { return user.name + " is " + user.age;}console.log(describeUser({ name: "Asha", age: 21 }));console.log(describeUser({ name: "Ravi" }));Asha is 21
Ravi is undefinedThe second call forgot the age. JavaScript shrugged, and the sentence says "Ravi is undefined". Nothing warned us.
Typos in property names are quieter still.
const user = { name: "Asha", agee: 21 };console.log(user.age);undefinedWe wrote agee by mistake. JavaScript added a new property called agee, and when we asked for age we got undefined. And an in-ink field, like a configuration value that must never change, can be changed by anyone.
const config = { host: "localhost", port: 8080 };config.port = 9090;console.log(config.port);9090Remember: JavaScript objects have no required fields, no spell-check for property names, and nothing is protected.
Print the form
Write the shape once. Get errors at every misuse.
Here is the same function with its form printed. On the TypeScript side, the shape is called User.
function describeUser(user) { return user.name + " is " + user.age;}console.log(describeUser({ name: "Asha", age: 21 }));console.log(describeUser({ name: "Ravi" }));Tinted lines are the ones that differ
The type User = { ... } part is a type alias: a name for a type, so you can use it again. It says a User has a name that is text and an age that is a number. The function now says user: User, and the second call is refused, because { name: "Ravi" } is missing a box. The error even names the missing property.
Extra fields get caught as well. This is the typo from before.
type User = { name: string; age: number;};const user: User = { name: "Asha", agee: 21 };TypeScript says
error TS2561: Object literal may only specify known properties, but 'agee' does not exist in type 'User'. Did you mean to write 'age'?
When you write an object straight into a place that expects a User, TypeScript checks that it has the right properties and no unknown ones. It even suggests what you meant.
Now the optional box. A question mark after the name means "this property may be missing".
type Contact = { name: string; email?: string;};function emailOf(contact: Contact): string { if (contact.email === undefined) { return "no email on file"; } return contact.email.toLowerCase();}email?: string means the email is either text or not there at all. Watch what happens if you use it without checking first. In JavaScript, it crashes.
const user = { name: "Asha", age: 21 };console.log(user.email.toLowerCase());Tinted lines are the ones that differ
The checker sees that user.email might be undefined, and refuses to let you call a method on it. The cure is a check, as in emailOf above: look first, then use. Lesson 6 covers this properly, along with the ?. and ?? shortcuts.
Finally, the ink. The word readonly in front of a property means "set it when you create the object, and never again".
const config = { host: "localhost", port: 8080 };config.port = 9090;console.log(config.port);Tinted lines are the ones that differ
The assignment is refused. To get a changed copy, you build a new object rather than editing the old one, which the third practice problem asks you to do.
Remember: ? means "may be missing", readonly means "can't be changed", and everything else is required.
Shapes, not names
TypeScript compares what an object has.
Two things make objects in TypeScript click.
First: type or interface? There are two ways to name an object shape. They look alike, and for most code they do the same job.
type Point = { x: number; y: number };interface Pin { x: number; y: number;}const corner: Point = { x: 1, y: 2 };const marker: Pin = corner; // the names differ, but the shape is the sameThe two have the same shape, so one goes wherever the other is wanted. The differences are small. An interface can be extended with extends, and a type can be combined with &, which means "both of these".
interface Animal { name: string;}interface Dog extends Animal { breed: string;}type Cat = Animal & { indoor: boolean };const dog: Dog = { name: "Momo", breed: "Indie" };const cat: Cat = { name: "Mishti", indoor: true };An interface can also be written twice with the same name, and TypeScript merges the two into one. A type can't. This is called declaration merging, and you rarely need it in everyday code.
interface Settings { theme: string;}interface Settings { fontSize: number;}// Both declarations joined into one, so both fields are needed.const settings: Settings = { theme: "dark", fontSize: 16 };A good rule: pick one and stay with it. This guide uses type, because it can also name things that aren't objects, which later lessons need. Many teams choose interface for objects. Either is a fine answer.
Second: TypeScript is structural. It decides whether an object fits a type by looking at what the object has, not at what it was called or where it came from. There is no need to say "this object implements that type". If it has the right properties, it fits.
const pet = { name: "Momo", age: 3, tail: true };// pet has everything a User needs, and one extra. That is fine here,// because pet is a variable and not a fresh object literal.console.log(describeUser(pet));Momo is 3pet has a name, an age and a tail. A User only needs a name and an age, so pet fits, and the extra tail is fine. Why was agee an error, then? The difference is a fresh object literal: an object written directly where a type is expected. Fresh literals get the strict extra-property check, because a stray property there is almost always a typo. A variable that already exists doesn't.
Remember: TypeScript asks "does it have what's needed?", not "what is it called?". Fresh object literals also get an extra check for unknown properties.
Where objects bite
Three places the form can fool you.
Optional doesn't mean empty. In displayName, a nickname of "" (an empty string) is still a nickname. If you test it with a plain if (person.nickname), an empty one counts as missing. Compare against undefined when "not given" is what you mean.
readonly is shallow, and only for the checker. It stops assignments the checker sees. It doesn't freeze the object at runtime, and it doesn't reach inside a property that is itself an object. It is a promise to your teammates and your tools, not a lock.
A type doesn't check data that arrives while the program runs. If an object comes from a server, User is a hope, not a promise the data keeps. Lesson 0 showed this, and lesson 9 fixes it.
Also, don't reach for any or for a cast (writing as User to overrule the checker) when a shape fights you. Fix the shape, or fix the data. Lesson 11 says when a cast is fair.
Remember: readonly and ? are promises that the checker holds you to. They are gone when the program runs.
Three shapes, three jobs
Name the shape, make a box optional, protect the ink.
Each problem starts with a JavaScript object function. Open it in the Playground, and describe the shape.
Problem 1
Name the shape
Describe the object fullName expects, and use that type for its parameter. The call missing last should be refused. Remove it.
Start here
function fullName(person) { return person.first + " " + person.last;}console.log(fullName({ first: "Asha", last: "Rao" }));console.log(fullName({ first: "Ravi" })); // "Ravi undefined"Stuck?
Show a solution
type Person = { first: string; last: string;};function fullName(person: Person): string { return person.first + " " + person.last;}fullName({ first: "Asha", last: "Rao" }); // "Asha Rao"The type lists exactly what the function reads: first and last, both strings. The function then asks for a Person. The call that forgot last is refused with 'Property last is missing', where JavaScript produced the text 'Ravi undefined'.
Problem 2
An optional nickname
Describe the person, with an optional nickname. displayName gives the nickname if there is one, and the first name if there isn't.
Start here
function displayName(person) { return person.nickname ? person.nickname : person.first;}console.log(displayName({ first: "Asha", nickname: "Ash" }));console.log(displayName({ first: "Ravi" }));Stuck?
Show a solution
type Named = { first: string; nickname?: string;};function displayName(person: Named): string { return person.nickname !== undefined ? person.nickname : person.first;}displayName({ first: "Ravi" }); // "Ravi"nickname?: string makes the property optional, so both calls are fine. Comparing with undefined says exactly what we mean, which is 'was one given'. The earlier truthy check treated an empty nickname as missing, which is a quiet bug in the JavaScript.
Problem 3
Protect the defaults
withPort changes the config you give it, which also changes the shared defaults. Make the properties readonly, and return a new object instead.
Start here
const defaults = { host: "localhost", port: 8080 };function withPort(config, port) { config.port = port; return config;}withPort(defaults, 9090);console.log(defaults.port); // 9090, so the defaults were changed behind our backStuck?
Show a solution
type Config = { readonly host: string; readonly port: number;};const defaults: Config = { host: "localhost", port: 8080 };function withPort(config: Config, port: number): Config { return { ...config, port };}withPort(defaults, 9090); // a new object; defaults is untouchedreadonly makes the assignment config.port = port an error, which is what pushes you to build a new object. The spread { ...config, port } copies every property and then replaces port. The defaults object stays untouched, which the JavaScript version did not manage.
Objects in the interview
Shapes, optional fields, and the type or interface question.
Expect the "type or interface" question in almost every TypeScript interview. The honest answer is short.
- "What is the difference between
typeandinterface?" For object shapes they are nearly the same. Aninterfacecan be extended withextendsand merged by declaring it twice. Atypecan also name unions, tuples and other non-object types, and combines with&. Pick one and be consistent. - "What does
?after a property name mean?" The property is optional: it may be missing, so its type includesundefined. - "What does
readonlydo?" The checker refuses assignments to that property. It doesn't freeze the object at runtime. - "What is structural typing?" Types are compared by their shape, by what properties they have. Two types with the same shape are interchangeable, whatever they are called.
- "Why is an extra property an error here but not there?" Object literals written directly where a type is expected get an excess-property check. An existing variable with extra properties is fine.
- "Do types check data from an API?" No. Types are erased at runtime. Data from outside needs a runtime check.
- Say it simply. "A type is the shape of an object. TypeScript checks shapes, not names" is a good answer.
Updated 2026-10-17
Made by VeraStack Labs