In GraphQL, how does a type resolver on the abstract type differ from a per-type predicate?
answer
- Same contract, two homes for the knowledge
- Central function versus one boolean per type
- Who edits what when a type is added
- First match wins, in unspecified order
- Cost scales with the possible-type count
basics
~20 sOne function on the interface or union inspects the value and names its object type. A per-type predicate instead lives on each object type and answers "is this value mine?", with the executor trying candidates until one says yes.
solid answer
~50 sBoth satisfy the same contract — produce one possible type of the abstract type, or fail — but they invert where the knowledge lives. A **central type resolver** is one function attached to the interface or union: it sees the resolved value, returns a type name, and gives you a single place where you can read off whether every member is handled. Its weakness is that every new member type means editing that one function. A **per-type predicate** is a boolean on each object type, and the executor tries the abstract type's possible types until one answers yes; a new type ships its own answer with nothing central to edit. Its weaknesses are that the trial order is not specified, so overlapping predicates give an arbitrary winner, and that cost is proportional to the possible-type count — 7 implementers over 4,312 contributions is up to 30,184 predicate calls. Neither shape is mandated: the specification fixes only the contract.
code
pseudocode · 5 linesresolveType(value) for union RecordDonationResult:
if value.outcome == "RECORDED": return "DonationRecorded"
if value.outcome == "DUPLICATE": return "DuplicateDonation"
if value.outcome == "REJECTED": return "DonationRejected"
raiseFieldError("no possible type for outcome " + value.outcome)go deeper
Know that both shapes exist and that they answer the same question from different places: one function on the interface or union, or one yes/no answer on each object type that could be the value.
Be ready to compare them on the three axes an interviewer probes: where the edit lands when a type is added, whether coverage is visible anywhere, and how the cost per resolved value scales with the number of possible types.
Demonstrate that you would make coverage provable rather than hoped for — an exhaustive mapping that fails the build, or a test that enumerates possible types from the schema — and that you treat overlapping predicates as a defect with no defined winner.
Frame it as an ownership decision: a closed set owned by one team wants the central resolver, a set fed by many modules wants colocated predicates plus an enforced coverage test. Set that standard once rather than per schema.
## One contract, two places to put the knowledge The specification says only what abstract type resolution must *achieve*: given an abstract type and a resolved value, produce an object type that is a possible type of it, or raise a field error. It leaves the mechanism to the type system, and two shapes have become near-universal across servers in every language. They are worth telling apart because they fail differently, cost differently and evolve differently. ## Shape A — one resolver on the abstract type A single function hangs off the interface or union. It receives the value the field resolver produced (servers commonly also hand it the request context and information about the field being executed, though that is convention, not a specified signature) and returns the name of an object type. What this buys you: * **A readable exhaustiveness check.** Coverage of the possible-type set is visible in one function. If a member is unhandled, a reviewer can see the gap by reading twelve lines. * **Access to everything at once.** The function can compare shapes, read a discriminator column, consult request context, or fall through a chain of increasingly specific tests. * **One decision, one call.** The cost per value is constant regardless of how many possible types the abstract type has. What it costs you: the function is a central edit point. Every new member type is a change to a file that the team adding the type may not own, which is exactly the coupling that produces the classic failure — the schema grows a member and the resolver does not. ## Shape B — a predicate on each object type Each object type carries a boolean: *given this value, are you me?* The executor walks the abstract type's possible types and uses the first that answers yes. What this buys you: * **Colocation.** The knowledge of what a `GrantAward` looks like lives with `GrantAward`. A new type is additive: define it, give it a predicate, done. * **Extensibility across ownership boundaries.** Modules that contribute types to a shared interface do not have to edit a shared function. What it costs you: * **No single view of coverage.** Nothing anywhere states that the set of predicates is total. An unmatched value is discovered at run time. * **Order is not specified.** If two predicates can both answer yes for one value, which one wins is whatever order the server happens to try them in — stable in practice, unspecified in principle, and liable to change when the schema is rebuilt. Predicates must be mutually exclusive, and nothing enforces that. * **Linear cost.** With 7 implementers, resolving one value may take 7 predicate calls; over a list of 4,312 contributions that is up to 30,184 calls. Harmless if each is a field comparison; expensive if a predicate touches a database or deserializes. ## The third thing servers do Many servers derive the answer without either hook: from the runtime class of the value, from a registered map of class to type name, or from a type-name-ish attribute on the fetched record. That is a convenience layer over the same contract. Two things to say about it in an interview. First, it is the shape most likely to break silently, because nothing in code names the mapping. Second, it fails badly when the value is a generic map or a row object with no distinguishing class, which is precisely the situation in a service reading heterogeneous rows from one table. ## Choosing between them The question is really about who owns the possible-type set. A closed set owned by one team is better served by a central resolver, ideally written so that adding a member fails the build rather than the request. A set that grows from several modules, or plugin-style, wants the predicate shape, plus a test that enumerates the abstract type's possible types from the schema and asserts every one is reachable — the coverage guarantee the shape does not give you on its own. A good answer also names the property both shapes must have: **totality over the possible-type set, and mutual exclusivity within it.** Everything else is a matter of where you keep the code.
- If two per-type predicates both answer yes for the same value, which type wins?Whichever the server happens to try first — the order in which possible types are tested is not specified, and it can change when the schema is rebuilt or the type registration order shifts. Treat overlapping predicates as a bug, not as a precedence rule, and make each one test a distinct discriminator value rather than a subset of fields.
- Which shape would you choose for an interface whose implementers are contributed by several modules?The per-type predicate, because a module can ship its type and its own answer without editing a function another team owns. Pay for that with a test that reads the interface's possible types from the schema and asserts each one is reachable, since the predicate shape gives you no single place to see that coverage is total.
- Does the specification say which of these two mechanisms a server must use?No. It defines only the contract: the type system must supply resolution that returns one possible type of the abstract type, or the field errors. The central resolver and the per-type predicate are widespread ecosystem conventions, and many servers also derive the type from a runtime class or a discriminator attribute without either hook.
One receptionist who recognises every visitor, versus every department putting a sign on its own door and the visitor trying each door until one fits.
saying these in an interview costs you the question
- Claims the specification mandates one of the two shapes
- Thinks predicates are tried in schema declaration order
- Says overlapping predicates are fine, most specific wins
- Ignores that predicate cost scales with implementer count
- Believes a central resolver can return several candidate types
- Assumes deriving the type from a runtime class always works