skip to content

Type Parameters

How a placeholder type is introduced and filled in: where it is declared, how arguments get inferred, and what interacting placeholders imply. Interviewers start here before bounds and variance.

on this pageshow

questions

16

A connection type carries a message-type placeholder and a standalone helper carries its own - how long is each fixed?

level: juniorimportance: must knowfreq 62%

answer

  1. scope follows the declaration site
  2. one fixed per instance, one per call
  3. shared name ties members together
  4. no instance means no shared placeholder
  5. used once in a signature constrains nothing

basics

~20 s

A placeholder introduced on the type is chosen once when an instance is created and every member of that instance shares it. A placeholder introduced on one operation is chosen afresh for each call and is scoped to that call.

solid answer

~40 s

The declaration site sets the scope. When the placeholder is introduced on the type header, it is filled in when you build the instance, and from then on the stored state and every operation of that instance all mean the same type by it - a connection made for chat frames sends and receives chat frames for its whole life. When the placeholder is introduced on a single operation's signature, it is filled in per call: the same helper can be called with one type now and another type on the next line, and nothing about the call is remembered afterwards. A free-standing helper, which has no instance behind it, has nowhere else to put a placeholder, so it must introduce its own.

code

pseudocode · 9 lines
pseudocode
type Connection<M>:
    outbox: list of M
    function send(message: M)
    function receive() -> M

function firstOrNone<T>(items: list of T) -> T or none:
    for each item in items:
        return item
    return none

go deeper

for a junior

Recall the one-line rule: a placeholder on the type is picked when the object is built, a placeholder on an operation is picked at each call. Be able to point at which one a given signature uses.

for a middle

Explain why the scope matters: a declaration-level name is what makes stored state and several operations agree on one type, while an operation-level name only has to hold together within a single call.

for a senior

Show that you read the site as an API commitment - how many objects a caller ends up holding, and which agreements between members the compiler is checking for you at no cost.

for a principal

Frame it as an interface-stability choice: where a library puts its placeholders decides whether callers commit once per object or once per call, and moving one later is a breaking change across every call site.

## Two places a placeholder can be introduced A **generic declaration** introduces a name - a **type parameter** - that stands in for a type the code does not yet name. That name has to be introduced somewhere, and there are only two places it can go: on the **declaration as a whole** (the type, and therefore everything the type contains), or on **one operation** inside it (a single method or function signature). The choice looks like syntax and is really about **scope**, and scope decides who has to agree about the type. Take a chat client's connection. Parameterized as a whole, the connection is built *for* a message type: its outgoing buffer holds that type, `send` accepts that type, `receive` produces that type. Parameterized per operation, the connection itself names no message type, and each operation invents one of its own for the duration of a single call. ## What each site means concretely | Introduced on | Chosen when | Stays fixed for | Who can refer to it | |---|---|---|---| | The type declaration | The instance is built | That instance's whole life | Stored state and every member | | One operation | Each call | That one call | Only that operation's signature and body | So a declaration-level placeholder is a promise **between members**: whatever the outbox holds, `send` accepts and `receive` returns. An operation-level placeholder is a promise **inside one signature**: whatever came in as an argument is what comes back out, and the next call is free to be about something else entirely. ## Why a free-standing helper has no choice An operation that belongs to no instance - a helper function on its own, or a member that is not attached to a particular object - has no instance to carry a type argument, so there is nothing for a declaration-level placeholder to be fixed *by*. A helper that returns the first element of a list, or that copies items from one sink to another, therefore introduces its placeholder on its own signature. This is the everyday case that makes the distinction visible: the helper is generic, but nothing about it is remembered between calls. The same reasoning explains a rule that trips people up when they meet it from the other direction: state that is shared by all instances of a type cannot be typed by the type's placeholder, because that placeholder has a different value for each instance, and shared state has only one. ## Reading a signature for its declaration site When you look at an unfamiliar signature, ask three questions in order: 1. **Does the name appear in more than one member?** If the buffer, the sender and the receiver all mention it, it is one shared name, and it belongs on the declaration. 2. **Must two calls agree?** If a value handed to one call has to come back out of a later call on the same object, the placeholder has to outlive a call, so it belongs on the declaration. 3. **Is the name used only within one signature and its body?** Then it belongs to that operation, and hoisting it up would over-constrain every caller. A useful sanity check on the result: an operation-level placeholder that appears **exactly once** in its own signature is usually a mistake, because a name used once constrains nothing - it is a way of writing *any type at all* with extra ceremony. ## What the caller feels - With a declaration-level placeholder, the caller commits **once**, at construction, and then every call is checked against that commitment for free. - With an operation-level placeholder, the caller commits **per call**, and the object it is calling through stays usable for any type. - Mixing them is normal and not a contradiction: a connection fixed to one message type can still expose a request-and-await operation whose *reply* type is chosen per call, because that reply type is genuinely independent of the connection's message type. Languages differ in how the two sites are spelled, and in what they allow an operation to do with a name the enclosing declaration already uses, but the scoping rule itself is the same everywhere: **a name introduced on a declaration lives as long as an instance of that declaration; a name introduced on an operation lives as long as a call.** Everything else about type parameters - how the argument is worked out at a call site, what may be required of it, and what survives compilation - sits on top of this one distinction.

  • Two calls to the same operation-level placeholder pick different types - is that a problem?
    No. Each call is checked on its own, and the name is scoped to the call, so one call may be about frames and the next about acknowledgements. That independence is exactly what the operation-level site is for; if the two calls did have to agree, the name would belong on the declaration instead.
  • Can one declaration use both sites at once?
    Yes, and it is common. A connection parameterized by its message type can still expose an operation that introduces a second, independent placeholder for something the connection does not store - a reply type, a decoded payload, a mapped result. The two names are unrelated, and each keeps its own scope.

A shipping firm that chooses one crate size the day it opens and handles only that size ever after, versus a courier who takes whatever box you hand him for this one trip and forgets it afterwards.

saying these in an interview costs you the question

  • Says a type-level placeholder is re-chosen on every call.
  • Thinks a function outside any type cannot introduce a placeholder of its own.
  • Believes all instances of a parameterized type share one chosen argument.
  • Treats the declaration site as pure syntax with no scope consequence.
  • Assumes an operation-level placeholder is remembered between calls on one object.
open as a page

When a generic filter helper is called without an explicit type argument, what does the compiler use to fill its placeholder?

level: juniorimportance: must knowfreq 58%

basics

~20 s

The compiler solves the placeholder from evidence at the call site: the static types of the arguments, the type the result is expected to have where the call sits, and any bound or default declared on the parameter itself.

open as a page

Why is a routing registry that maps route keys to handler payloads declared with two placeholders rather than one?

level: juniorimportance: must knowfreq 60%

basics

~20 s

A key type and a payload type vary independently, so each needs its own placeholder. A placeholder takes one type argument per use, so a single placeholder would force every route key and its payload to be the same type.

open as a page

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%

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.

open as a page

Why must a connection's outbox, send and receive share one declaration-level placeholder rather than each introducing its own?

level: middleimportance: must knowfreq 57%

basics

~20 s

Only a placeholder introduced on the declaration is one name that all members mean the same thing by. Give each operation its own and nothing ties them together: a value put in and a value taken out need never match, and the stored buffer agrees with neither.

open as a page

A generic filter helper is called with an empty basket that names no element type, so how is the placeholder resolved?

level: middleimportance: must knowfreq 62%

basics

~20 s

An empty input contributes no evidence, so the placeholder has to come from elsewhere: the expected type at the call's position, a declared bound or default, or an explicit type argument. Otherwise the call widens uselessly or is rejected.

open as a page

Why can a generic call that compiles on its own line fail when nested directly inside another generic call?

level: middleimportance: should knowfreq 44%

basics

~20 s

On its own line the call takes an expected type from the variable's declaration. Nested as an argument it has none, because the surrounding parameter is itself a placeholder, so two unknowns have to be solved together and neither side supplies the other.

open as a page

In a registry signature where the payload placeholder must be the type the chosen route declares, what does the caller actually choose?

level: middleimportance: should knowfreq 55%

basics

~20 s

A caller chooses the route only. When one placeholder is expressed through another, that single choice settles both positions: the payload is determined rather than chosen, and a payload argument that disagrees is rejected at the call site.

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 team moved a per-call helper's placeholder up onto the connection declaration - why must callers now hold several connections?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A placeholder on the declaration is fixed when the instance is built, so one instance can serve only one type. Work that used to pick its type per call now picks it per object, and a caller handling several types needs one connection for each.

open as a page

When a basket helper's inferred type argument is ambiguous or wider than intended, what decides between naming it explicitly and restructuring the call?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Count the call sites. One call that genuinely knows a type the solver cannot see deserves an explicit type argument; the same annotation appearing at many call sites accuses the signature, which is handing the solver too little evidence.

open as a page

Your registry requires a handler declared over exactly the route's payload type, and a working shared handler is rejected — what over-constrained the signature?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Stating the relation between the two placeholders as identity rather than as what the body needs. The body only requires that the handler accept the route's payload; demanding the handler be declared over that exact type rejects broader handlers that already accept it.

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

An operation introduces a placeholder named the same as its enclosing type's - which one does that operation's body see?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

The operation's own placeholder wins inside that operation. The inner name hides the enclosing type's for the whole body, so the two are unrelated types that happen to share a spelling, and stored state typed by the outer one will not accept the inner one.

open as a page

When does adding a third placeholder to a registry declaration signal that the type is doing two jobs?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

When the placeholders partition: no member mentions all of them, and different call sites care about different subsets. That means two responsibilities were glued into one declaration, and the fix is to split the type rather than to keep counting.

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