skip to content

Reuse Without Casting

Why abstract over a type at all: the same code duplicated per type, or funnelled through a top type and cast back, versus one checked implementation. Interviewers ask for the trade-off, not syntax.

on this pageshow

questions

4

A lost-property desk stores every item as one universal supertype and casts on collection — what does a type parameter buy instead?

level: juniorimportance: must knowfreq 70%

answer

  1. the deposit knew more than the store
  2. every retrieval carries a cast
  3. who finds the wrong item, and when
  4. collection time versus compile time
  5. one body the compiler can check

basics

~20 s

A type parameter moves the wrong-kind failure from collection time to the deposit call, and from run time to compile time. Callers also get items back already typed, so no downcast on retrieval can fail.

solid answer

~40 s

Storing everything as a universal supertype throws away what the caller knew at the moment of deposit. The code compiles, but every retrieval has to downcast, and a wrong deposit is only noticed when someone collects that item — a different line, in different code, from the one that made the mistake. Declaring the desk over a placeholder `T` keeps that knowledge in the signature: `deposit(tag, item: T)` rejects the wrong kind at the call site, `collect(tag): T` hands back something already typed, and no cast appears in caller code at all. You pay a denser signature and you must decide what `T` is at each use. What you get is one implementation the compiler checks once, in terms of `T`, instead of one implementation it cannot check at all.

code

pseudocode · 11 lines
pseudocode
// design A: one desk, every item stored as the top type
deskA = Store()
deskA.deposit("t1", umbrella)
deskA.deposit("t2", ticketStub)      // accepted: everything is an item
u = downcast(deskA.collect("t2"), Umbrella)   // FAILS here, at collection

// design B: one desk, the kind left as a placeholder
deskB = Store<Umbrella>()
deskB.deposit("t1", umbrella)
deskB.deposit("t2", ticketStub)      // REJECTED here, at the deposit
u = deskB.collect("t1")             // already an umbrella, no downcast

go deeper

for a junior

Recall the two costs of storing everything as one universal supertype: every retrieval needs a downcast, and a wrong deposit is not noticed until someone collects it. Say what the placeholder replaces those with.

for a middle

Explain the mechanics: the deposit parameter's type is what makes the call site checkable, and the collect return type is what removes the cast. Point out that the shared body is checked once, in terms of the placeholder, for every kind at once.

for a senior

Frame it as failure reporting. The interesting property is that the mistake is reported at the line that made it rather than at whoever collected, which is what changes debugging time in a real system.

for a principal

The trade you own is where the typed boundary sits. Decide which entry points are checked and where mixed content is genuinely required, because a desk that must hold anything reintroduces the downcast and should do so deliberately, not by accident.

## Three ways to write one desk A lost-property desk does three things — take an item in against a tag, hold it, hand it back — and none of them depend on what the item *is*. The tag index, the expiry sweep and the hand-back are identical whether the desk holds umbrellas, laptops or coats. Only the **kind of thing held** varies. Three designs are possible, and the interview question is why the third is not merely the second with extra punctuation. - **One desk per kind.** Copy the whole store and change the item type in the field, the deposit parameter and the collect return. Three bodies. - **One desk over a universal supertype.** Declare the held field as the **top type** — the type every value in the language belongs to. One body, but the desk now knows nothing about what it holds. - **One desk with a placeholder.** Declare the desk over a **type parameter** `T` and use `T` for the field, the deposit parameter and the collect return. One body, and each *use* of the desk fixes `T` to one concrete kind. ## What the top-type desk gives away At the deposit call the calling code knows exactly what it is handing over. The top-typed desk refuses to record that fact: its parameter accepts anything, so the knowledge dies at the door. Two consequences follow, and both are about **when and where a mistake is reported**. 1. **Retrieval needs a downcast.** `collect(tag)` can only promise "an item". The caller asserts the kind it expects — and that assertion is a *promise by the programmer*, not a fact the compiler established. If the promise is wrong, the only way to find out is to run it. 2. **A wrong deposit is invisible.** Handing a ticket stub to an umbrella desk compiles and runs perfectly. Nothing appears wrong until someone collects that tag, and the failure then surfaces on the collector's line — different function, different call stack, possibly a different team's request. Candidates usually undersell the second point. The downcast does not *cause* the failure; it is only the first place the mismatch can be noticed. The defect was the deposit, and this design has no way to point back at it. ## What the placeholder puts back A desk declared as `Store<T>`, with `deposit(tag, item: T)` and `collect(tag): T`, restates in the signature the thing the caller already knew: - the **deposit call is checked** against that desk's own kind, so the ticket stub is rejected on the line that offered it, before the program runs; - the **collect call needs no cast**, because its result type is `T`, and at that use site `T` *is* the concrete kind; - there is still **one body**, which the compiler checks once in terms of `T`, and that single check covers every kind anyone will ever instantiate it with. That last point is the one people forget. Copy-pasting per kind also gives you checking — three copies are each checked — but at the price of three bodies to fix. The top type gives one body at the price of the checking. The placeholder is the only one of the three that gives both at once. ## Side by side | | desk per kind | one desk, top type | one desk, placeholder | |---|---|---|---| | bodies to maintain | three | one | one | | wrong kind reported | at the deposit call | at collection, when the downcast runs | at the deposit call | | caller's retrieval code | already the right kind | must downcast | already the right kind | | adding a fourth kind | another full copy | nothing to change | one new use site | | what the compiler verifies | each copy separately | that the item is *something* | the one body, for every `T` | ## What it costs It is not free, and an interviewer likes hearing the cost named: 1. The **signature is denser** to read, and the reader has to hold a placeholder in their head while reading the body. 2. The caller must **decide the kind at each use**. A desk that genuinely has to hold mixed items is not this design; widening to a shared supertype there brings the downcast back, honestly and on purpose. 3. The guarantee is a **compile-time** one, established at the typed entry points. A path that reaches the stored items without going through those entry points is outside what the placeholder can promise. ## The two adjacent claims that are wrong The placeholder is not a **performance** feature: how the shared body is compiled is a separate decision, and the deposits and collections themselves cost what they cost either way. And removing casts is not a matter of tidiness — the cast's real price is not the characters it takes, it is that the failure gets reported at the wrong end of the program, by the innocent party.

  • If a desk is only ever used with one kind of item, why not just write that kind into it directly?
    You should. A concrete type is the right answer for one kind — it is shorter to read and checks just as well. The placeholder earns its keep from the second use: two or more kinds, or a caller that chooses the kind, is what turns a concrete desk into a copy-paste problem.
  • The desk also needs a method that lists everything it currently holds. What does that method return in each design?
    In the top-typed design it returns a collection of top-typed values, and the caller downcasts every element it wants to use — the same failure, now once per element. In the parameterized design it returns a collection of `T`, so each element arrives as the concrete kind and the loop body needs no cast.

saying these in an interview costs you the question

  • Casting is cheap, so the two designs are basically equivalent
  • A type parameter is a performance feature that avoids conversions
  • The wrong deposit fails at the deposit, so it is caught early anyway
  • A universal supertype is fine because we only store one kind in practice
  • Placeholders exist mainly to save typing in the declaration
open as a page

Three near-identical item stores were copy-pasted, one per item kind — what does collapsing them onto one type parameter actually change?

level: middleimportance: should knowfreq 52%

basics

~20 s

One body replaces three, so a fix lands once and reaches every item kind, while each use site is still checked against the kind it chose. The cost is that per-kind special cases lose their home inside the store.

open as a page

A wrongly deposited item in a cast-based store only fails when someone collects it — where does a type parameter move that failure?

level: seniorimportance: should knowfreq 44%

basics

~20 s

To the deposit call itself, and from run time to compile time. The line that made the mistake is the line that fails, so the report names the code responsible instead of whoever collected the item.

open as a page

Three item stores look near-identical — what would tell you that folding them onto one type parameter is the wrong move?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

That they differ in more than the item kind. If the bodies diverge in rules, invariants or reasons to change, the shared version pays for its single body with per-kind switches, and three honest bodies read better.

open as a page