Objects: State, Behavior, Identity
Beginner · 10 min read · ▶ live playground · ✦ checkpoint
Two copies of the same novel sit on different shelves. A reader borrows one, but your program marks both as gone because it tracks only the title. The missing idea is not another line of code; it is knowing which particular thing changed.
An object is a particular thing in a running program that combines state, the data it currently holds, with behavior, the operations it can perform, and identity, what keeps it distinct from other things. Those three parts let your design answer three different questions: what does this thing know, what can it do, and which thing is it?
A class is the blueprint that describes the fields and methods its objects receive. An instance is one object made from that class. LibraryCopy can be the class, while the copy labeled COPY-17 is one instance.
Read one object-shaped design
Imagine a library owns two physical copies of The Long Way Home. Their title may match, but lending one must not change the other. The copy number carries identity, the current holder belongs to state, and checkout is behavior.
class diagram
A library copy keeps its own lending state
- 1`copyId`, `title`, and `holder` are the state stored by this LibraryCopy.
- 2`checkoutTo()` and `returnToShelf()` are behavior that changes the current holder.
- 3`copyId` keeps two physical copies distinct even when their titles match.
The diagram shows a class, not a live object. It lists the shape every LibraryCopy instance receives. The current value "Mina" does not belong in the class diagram because another instance may still have no holder.
Turn the blueprint into instances
A constructor is the special class method that sets an instance's starting state. Writing new asks the class to create an instance and run that constructor.
The field type string | null is a union type, a type that permits one of several listed choices. Here, a holder is either a reader's name or null, which means the copy is on the shelf.
class LibraryCopy {
public copyId: string;
public title: string;
public holder: string | null;
public constructor(copyId: string, title: string) {
this.copyId = copyId;
this.title = title;
this.holder = null;
}
public checkoutTo(readerName: string): void {
this.holder = readerName;
}
public returnToShelf(): void {
this.holder = null;
}
public describe(): string {
if (this.holder === null) {
return `${this.title} (${this.copyId}) — on shelf`;
}
return `${this.title} (${this.copyId}) — held by ${this.holder}`;
}
}
const eastWingCopy = new LibraryCopy("COPY-17", "The Long Way Home");
const windowShelfCopy = new LibraryCopy("COPY-42", "The Long Way Home");
eastWingCopy.checkoutTo("Mina");
console.log(eastWingCopy.describe());
console.log(windowShelfCopy.describe());The Long Way Home (COPY-17) — held by Mina The Long Way Home (COPY-42) — on shelf
Both objects come from the same class. They do not share their field values. Calling checkoutTo("Mina") on eastWingCopy changes that instance's holder; windowShelfCopy keeps its own state.
This is the first common misconception to remove: a class does not represent one giant shared record. It defines what each made object contains and can do.
State changes; identity answers “which one?”
State is a snapshot. The east-wing copy moves from the shelf to Mina and later back to the shelf. Its holder changes each time.
Identity stays attached to the particular thing through those changes. In the library's records, COPY-17 means the east-wing copy even when its holder and shelf location change. Two copies can have the same title and the same lending status while remaining two distinct objects.
The running program also keeps the instances separate. A reference is a value that points to a particular object. eastWingCopy and windowShelfCopy hold references to different instances, so a method call through one reference reaches only that object.
Behavior belongs beside the state it changes
Methods turn a passive bundle of fields into an object that can participate in a flow. checkoutTo() says what happens in the language of the problem. The caller asks a copy to check out; it does not need to assemble a new copy record by hand.
That does not mean every method is automatically good design. changeEverything(copyId, title, holder) would expose storage details rather than express a useful action. Strong behavior uses the verbs of the problem and changes the state that belongs to that object.
Our first version still has a deliberate weakness: every field is public. Outside code can write eastWingCopy.copyId = "COPY-42" and damage the identity rule. The class groups state and behavior, but it does not yet control how state changes. That is the next lesson's job.
When not to make it an object
Not every noun needs an identity-bearing class. A value is data whose meaning comes entirely from its content. If two values contain the same data and are interchangeable, tracking which particular value you hold adds nothing.
Consider an ingredient amount. Two records that both mean 200 grams of flour do not need separate histories. A plain record is a small group of named fields without a class of its own, and it carries this idea well:
type IngredientAmount = {
ingredient: string;
grams: number;
};
function scaleAmount(
amount: IngredientAmount,
factor: number,
): IngredientAmount {
return {
ingredient: amount.ingredient,
grams: amount.grams * factor,
};
}
const flour = scaleAmount(
{ ingredient: "flour", grams: 200 },
1.5,
);The result is 300 grams of flour. There is no meaningful “original flour amount” whose identity must survive the calculation. A record and a function keep the model honest and small.
Start considering a class when something has a life over time, owns rules, or must remain distinguishable from similar things. Use a value when content is the whole meaning. This is a modeling choice, not a rule imposed by TypeScript: a plain record is technically an object-shaped JavaScript value, but your design can still treat it as interchangeable data.
Checkpoint
Answer all three to mark this lesson complete
You can now separate a blueprint from the individual objects it creates, and you can ask whether identity belongs in the model at all. Next, you will protect an object's rules by controlling how outside code reaches its state.