skip to content

What must an object type do to validly implement an interface?

level: middleimportance: should knowfreq 54%

answer

  1. Nothing is inherited; everything is repeated
  2. Two rules pointing opposite directions
  3. Return types may narrow, never widen
  4. Arguments must match exactly
  5. Extra arguments only if not required

basics

~20 s

Declare every field the interface declares, returning the same type or a narrower subtype, and repeat every interface argument at exactly the same type. Extra arguments must not be required, and transitively implemented interfaces must also be listed.

solid answer

~50 s

Interface implementation is checked structurally when the schema is built, and the rules are asymmetric on purpose. **Fields:** the implementing type must declare every field name the interface declares — there is no inheritance, you repeat them. **Return types** are covariant: the implementer may return the same type, a Non-Null wrapper where the interface allowed null, or a concrete type that implements the interface (or is a member of the union) the interface field returned; it may never widen a `String!` to `String`. **Arguments** are invariant: every argument the interface field declares must appear on the implementing field with exactly the same type, and the implementer may add further arguments only if they are nullable, so a caller writing against the interface never has to supply something it does not know about. **Transitivity:** if the interface implements another interface, the object type must list that one too.

code

graphql · 12 lines
graphql
interface CaseRecord {
  id: ID!
  filedAt: String
  relatedTo(caseNumber: String!): [CaseRecord!]
}

type Motion implements CaseRecord {
  id: ID!
  filedAt: String!
  relatedTo(caseNumber: String!, sealed: Boolean = false): [Motion!]!
  hearingDate: String
}

go deeper

for a junior

Be ready to say that an implementing object type repeats every field of the interface itself — nothing is inherited — and that a missing field means the schema will not build. Recognising a valid implementation in a short SDL snippet is enough here.

for a middle

Explain both directions: return types may narrow (add non-null, pick a concrete implementer) while argument types must match exactly, and extra arguments must not be required. Be able to justify each direction from the caller's point of view.

for a senior

Show you can read a schema-build error and name the offending rule immediately, and that you think about blast radius — widening an interface is a change across every implementer, enforced as a hard failure rather than a deprecation.

for a principal

Own the shape of the abstract layer: how wide interfaces get, who may add to one, and whether a shared interface across teams is a contract worth the coordinated change it forces on every implementer.

## Nothing is inherited — everything is repeated The first surprise for people arriving from an object-oriented language is that `implements` in SDL copies nothing. An interface is a *contract expressed in the schema*, and the implementing object type must spell out every field of that contract itself. If `CaseRecord` declares `id`, `filedAt` and `relatedTo`, then `Motion implements CaseRecord` writes all three out. Omit one and the schema does not build. That is verbose, and it is deliberate: introspection and every client generator read the object type's own field list, so the type must be complete on its own terms rather than by reference. ## Return types are covariant A field's declared return type on the implementer must be a **valid subtype** of the interface field's type. Three moves are legal: 1. **The identical type.** Always safe. 2. **Adding non-null.** An interface field typed `String` may be implemented as `String!`. That only ever gives a caller more than the contract promised — code written against `String` already handles a non-null value. 3. **Narrowing an abstract type.** If the interface field returns `CaseRecord`, the implementer may return `Motion`, provided `Motion` implements `CaseRecord`. If the interface field returned a union, the implementer may return any member of that union. These compose through list wrappers, element by element: `[CaseRecord!]` may be implemented as `[Motion!]` or `[Motion!]!`, but not as `[Motion]` — that would widen the element from non-null to nullable. The direction is the whole point. A client that selected the field through the interface has a type in hand derived from the interface's declaration. Anything the implementer returns must fit inside that expectation. Making a value *more* certain fits; making it less certain does not, which is why removing a `!` on an implementing field is a validity error and not merely bad taste. ## Arguments are invariant, with a one-way escape hatch Arguments run the opposite way, and it is worth being able to say why in an interview. **Every argument on the interface field must appear on the implementing field with the same type.** Not a subtype, not a supertype — the same. A client writing `relatedTo(caseNumber: "2019-CV-4471")` against the interface must be legal no matter which concrete type the runtime object turns out to be, so no implementer may retype, rename or drop that argument. Even relaxing `String!` to `String` is rejected: argument types are compared for equality, not compatibility. **An implementer may declare additional arguments the interface does not**, but only if they are not required — that is, nullable, or Non-Null with a default value. A required extra argument would make the interface's own contract unsatisfiable, because a document written against the interface would never supply it. ## Transitive interfaces must be declared An interface may itself implement another interface. When it does, an object type implementing the narrower one must **also** list the wider one explicitly: `type Motion implements CaseRecord & Filing`. Again, nothing is inferred. The same completeness rule applies to interfaces implementing interfaces, and the implementation graph may not contain a cycle. ## What it looks like when it fails Schema-build failures here are usually one of four sentences: a field is missing; a field's type is not a valid subtype; an argument is missing or has the wrong type; a transitively implemented interface was not declared. The message names the type, the field and often the expected type, so the diagnosis is mechanical — the difficulty is only ever remembering which direction is allowed to move. In a legal case-file graph, the realistic version of this failure is a growing family of record types. `CaseRecord` starts with `id` and `filedAt`; six months later somebody adds `court: Court!` to the interface and the schema stops building until all eleven implementers are updated. That is the cost and the value of the rule in one event: the contract cannot drift silently, and adding to an interface is a change with a blast radius proportional to the number of implementers. ## The judgement it forces Because implementation is checked structurally rather than nominally-with-inheritance, a wide interface is expensive to widen and a narrow one is cheap. Interfaces that carry only the fields every implementer genuinely shares stay easy to extend; an interface that accumulated convenience fields turns every addition into a multi-type change. That is a design consequence of a validity rule, and interviewers like hearing the connection made.

  • May an implementing field return a type that is nullable where the interface said Non-Null?
    No. Subtyping runs one way: the implementer may add `!` where the interface allowed null, never remove one. A caller that selected the field through the interface holds a type saying the value is always present, so returning a nullable type would break that guarantee. The schema fails to build.
  • Why may an implementer add an argument but not a required one?
    Because a document written against the interface never mentions arguments it cannot see. If the runtime object turns out to be the implementer that added a required argument, that operation would suddenly be missing a required input with no way for the author to have known. Allowing only nullable or defaulted extras keeps every interface-shaped selection executable against every implementer.
  • What happens to the schema when a field is added to an interface with eleven implementers?
    It stops building until all eleven declare the new field, because implementation completeness is checked structurally with no inheritance. That is the practical argument for keeping interfaces narrow: the cost of widening one scales with the number of implementers, and the failure is a hard schema-build error rather than a warning.

saying these in an interview costs you the question

  • Thinks implementing types inherit the interface's fields
  • Believes an implementer may drop an interface argument
  • Says a Non-Null interface field may be implemented as nullable
  • Adds a required argument not present on the interface
  • Forgets to declare a transitively implemented interface
  • Thinks argument types may narrow like return types

context