Why can checking a loaded type's directly declared supertypes miss a contract the object genuinely satisfies?
answer
- the declared list is one level
- is-a is transitive
- contracts extend contracts
- closure walk with a visited set
- or ask assignability instead
basics
~20 sBecause the declared list is one level deep. A type records the type it directly extends and the contracts it directly declares, while those contracts extend further contracts and the extended type brings its own — so a genuine match needs the transitive closure, not the list.
solid answer
~40 sA type's description records its **direct** supertypes: the one type it extends and the contracts it names in its own declaration. Satisfying a contract is transitive, though — a declared contract can itself extend others, and the extended type drags its whole chain along — so a check against the direct list answers a narrower question than the one asked. Two ways out: ask the runtime's own assignability test, which already computes the closure and is the right tool when you have a candidate type in hand; or build the closure yourself with a breadth-first walk and a visited set, which is what you need when you want the *list*, for example to rank handlers by specificity. The visited set is not optional: a contract reachable by two routes will otherwise be reported twice.
code
pseudocode · 10 linesfunction allSupertypes(type)
seen = emptySet()
queue = [type]
while queue is not empty
current = queue.removeFirst()
for each parent in directSupertypesOf(current) // one level: base type + declared contracts
if parent not in seen then
seen.add(parent)
queue.append(parent)
return seengo deeper
Know that the supertypes listed on a type are only the ones it names directly, and that a type can still be usable as something that list does not mention.
Explain the transitivity and produce the closure walk, including the visited set and why it is needed when a contract is reachable by two routes.
Show the judgment on when not to walk at all: with a candidate type in hand the runtime's own test is exact and cheap, and the walk is justified only when the set itself is the product.
Decide whether dispatching on a discovered type graph belongs in your systems, given that specificity ranking has no universally right answer and every team that writes one invents different tie-breaking.
## What a declared supertype list holds When a type is described at run time, its relationship to other types comes back as a small, **direct** list: the single type it extends, where the model has one, plus the contracts named in its own declaration. That list is a faithful record of what the declaration said. It is not the set of types a value of this type may be treated as. The renderer case makes the gap concrete. The renderer keeps a table of specialised formatters keyed by contract — one for anything orderable, one for anything that can present itself as text. Handed a record, it reads the record type's declared contracts, finds no match, and falls back to the generic formatter, even though the record plainly satisfies one of them through its parent. ## Why one level is not the relation The *is-a* relation is **transitive**: if this type extends that one and that one declares a contract, this type satisfies the contract. Three separate routes make the closure bigger than the direct list: - The extended type carries its own declared supertypes, and so on up to the chain's root. - A contract may extend other contracts, so naming one pulls in everything above it. - In models that allow it, several contracts can be declared at once, each with its own ancestry, so the shape above a type is a graph and not a line. Because it is a graph, the same contract can be reachable by more than one route — reached through the parent type and declared again directly, for instance. That is legal and common, and it is why a naive walk reports duplicates. ## Two ways to answer the question | you want | use | why | |---|---|---| | yes or no for one candidate type | the runtime's assignability test | it computes the closure internally and is exact for that runtime's rules | | the whole set of supertypes | a closure walk you write | no single query returns it; you need the list, not a verdict | | the most specific match among several | a closure walk plus an ordering rule | the test says yes for several candidates at once and cannot rank them | Most mistakes here come from reaching for the list when a verdict would do. If you hold both types, the built-in test is shorter, faster and right by construction. The walk earns its place only when the *set* is the product — ranking handlers, reporting what a type conforms to, or deciding which of several matching formatters wins. ## Building the closure 1. Seed a queue with the starting type and an empty visited set. 2. Remove a type from the queue and ask for its **direct** supertypes only. 3. For each one not already in the visited set, add it and enqueue it. 4. Stop when the queue empties; the visited set is the closure. The visited set does double duty: it de-duplicates the diamond case and it terminates the walk. Ordering matters if you intend to rank: breadth-first gives you rough nearness to the starting type, which is a reasonable proxy for specificity, but it is only a proxy — two unrelated contracts can sit at the same distance and both match, and then you need an explicit tie rule rather than whichever the walk happened to reach first. ## What surprises the walker - In many models every chain ends at a common root type, so the closure of *anything* contains at least one entry that matches everything. A handler keyed on it silently becomes the default. - A contract declared both directly and inherited appears once in the closure and twice in a walk without a visited set. - Depth is not specificity. A deeply nested contract can be broader in meaning than a shallow one. ## Where type systems differ In a nominally typed model, conformance is declared, so there is a list to walk and the closure is exactly the answer. In a structurally typed model, a type satisfies a contract when its members line up, with nothing declared and nothing to traverse — discovery there compares shapes rather than following edges, and a type can satisfy a contract written after it. Some models also let conformance be added to an existing type from outside its declaration, in which case the declared list on the type is not even the full direct story. A component that must work across such models should ask the runtime whether a value fits rather than reasoning from a list it read.
- You have the closure and three handlers match — how do you choose one?Define an explicit ordering instead of trusting walk order. Prefer an exact match on the type itself, then nearer supertypes, then contracts; breadth-first distance is a usable proxy for nearness but it is only a proxy. Two unrelated contracts can match at the same distance, so you need a declared tie rule — a priority on the handler or an error — rather than whichever the walk reached first.
- How can a type satisfy a contract with no declared link at all?In a structurally typed model conformance is computed from members rather than declared, so nothing records the relationship and there is no edge to follow. A type written before the contract can satisfy it. Discovery in that world compares the shape of the type's members against the shape the contract requires, which is a different operation from walking supertype edges.
saying these in an interview costs you the question
- Treats the directly declared contract list as the complete set.
- Assumes a contract cannot itself extend other contracts.
- Walks supertypes without a visited set and reports duplicates.
- Thinks greater depth in the graph always means greater specificity.
- Assumes every type system records declared conformance to walk.