Saying that one type is a subtype of another asserts that its values behave acceptably anywhere the supertype is expected — not merely that the members line up. Which real languages and tools actually try to check that behavioural assertion mechanically, how far does each one get, and what do teams do on a stack where nothing checks it at all?
answer
- compiler checks shape; behaviour is unverified
- Go/TypeScript: matching shape IS the claim (accidental Stringer)
- Java/C# covariant arrays -> runtime store check
- SPARK / OpenJML: inherited contracts proved at build time
- Rust unsafe impl Send = a human signs for it
basics
~20 sCompilers check shape, not behaviour. Go and TypeScript infer the claim from a matching shape alone; Java and C# accept covariant arrays and repair them at run time; only proof tools like SPARK and OpenJML genuinely verify; Rust's unsafe impl admits the compiler cannot. Otherwise, run one shared conformance suite against every implementer.
solid answer
~50 sCompilers check that members line up; almost nothing checks that the object behaves. Four points on the spectrum: - **Go and TypeScript** need no declaration at all — a matching shape *is* the claim. A Go type given a `String() string` method for debugging silently becomes a `fmt.Stringer` and changes how every `%v` prints it. Nobody asserted anything; the shape asserted it. - **Java and C#** made arrays covariant: the type system accepts a `String[]` as an `Object[]`, then repairs the lie on every store — `ArrayStoreException` / `ArrayTypeMismatchException`. The claim is accepted statically and policed dynamically. - **SPARK/Ada and JML/OpenJML** really verify: inherited class-wide contracts become proof obligations discharged at build time, over the fragment of the language the prover supports. - **Rust** admits the limit in its grammar: `Send`/`Sync` are `unsafe` traits the compiler propagates but never verifies, so claiming one by hand needs `unsafe impl`. Where nothing checks, write one conformance suite per supertype and run it against every implementer.
code
text · 6 linesstrings : Array of String
alias : Array of Object = strings // accepted: arrays are covariant
alias[0] = 42 // compiles fine
// at run time the array still knows it is Array of String
// -> store check fails: ArrayStoreException / ArrayTypeMismatchExceptiongo deeper
Know the core recall: declaring a subtype is a promise about behaviour, and the compiler only checks that the methods exist with matching signatures. Be able to say that the rest is on the programmer.
Be able to name at least two concrete mechanisms — Java/C# covariant arrays repaired by a runtime store check, and Go/TypeScript accepting conformance from a matching shape nobody declared — and explain why each shows the check is about shape.
Lay out the spectrum from nothing checked, through runtime repair, to build-time proof, and land on the practical fallback: one conformance suite per supertype, run against every implementer including test doubles. Name the failure modes each level leaves open.
Frame it as where you choose to spend verification budget: proof tooling like SPARK or OpenJML for the small set of interfaces whose violation is catastrophic, conformance suites plus narrower interfaces everywhere else, and a deliberate policy on structural conformance (sealed interfaces, branding) so a subtype claim is never made by accident in a codebase many teams extend.
## What the declaration actually promises When you declare that `B` is a subtype of `A`, callers become entitled to hold a `B` through an `A`-shaped reference and get `A`-shaped behaviour: the operations return what `A` promised, leave the state `A` promised, and refuse what `A` said they would never do. That is a claim about *observable behaviour over time*. What a type checker verifies is far weaker — that members with compatible signatures exist — and then it trusts you. Everything between "the signatures line up" and "the object actually behaves like one" is unverified; the design discipline that fills the gap is the Liskov Substitution Principle, covered on its own. The interesting question is how far real systems get toward mechanising the check, and what you do at the many points where they get nowhere. ## Level 0 — structural systems assert the claim *for* you In Go, a type satisfies an interface simply by having methods with the right names and signatures; there is no `implements` and no declaration anywhere. In TypeScript the same is true of object types. The consequence is that a subtype claim can come into existence without any human ever making one. The canonical Go case: you add `String() string` to a type as a debugging helper, and it now satisfies `fmt.Stringer`, so every `%v` in the program routes through it — and if that method formats its own receiver with `%v` it recurses until the process dies. Adding `Error() string` silently makes a type an `error`. TypeScript has the mirror problem: any object literal with a matching shape is accepted where your carefully-named domain type was expected, however unrelated its meaning. Both languages have escapes that restore a *deliberate* claim: Go's sealed-interface trick (an unexported method in the interface, so only your package can satisfy it) and TypeScript's branding (a private field or a `unique symbol` property nobody outside can produce). Nominal systems — Java, C#, Kotlin, Rust — at least require a human to write the claim down, which is a social check, not a behavioural one. ## Level 1 — accept the claim, repair it at run time Java and C# both made arrays covariant: a `String[]` is usable as an `Object[]`. That is fine for reads and false for writes, and both languages knew it. Their answer was not to reject the claim but to police it: every array store carries a runtime type check, and a bad store throws `ArrayStoreException` in Java or `ArrayTypeMismatchException` in .NET. This is the clearest specimen of a type system knowingly accepting a behavioural claim it cannot honour and buying the difference back with a dynamic check. The motive was expressiveness before generics existed — you needed *some* way to write a routine over any array. The cost is a check on a very hot operation (which JITs then work hard to elide) and an error class that only fires when the offending write executes. ## Level 2 — systems that genuinely attempt the proof Ada 2012 lets a tagged type carry class-wide contracts, and **SPARK** discharges the resulting obligations *by proof at build time*: the prover reasons about every dispatching call, not only the ones a test happens to reach. **JML** with **OpenJML** does the same for Java — specifications are inherited by subclasses and the tool generates verification conditions asserting that each override still satisfies what it inherited. These are real mechanised checks of a behavioural claim, and their limits are equally real: they verify only what you wrote in the specification language, only over the language subset the prover supports, and they cost specification effort that most teams will not spend outside avionics, rail, or crypto. ## Level 3 — the language admitting it cannot check Rust encodes the limit in its grammar. `Send` and `Sync` are *unsafe* traits: the compiler auto-derives them structurally when every field already has them, and it propagates them through generic bounds — but it never verifies the semantic property that a value is genuinely safe to move to, or share with, another thread. When you need to claim one for a type built from raw pointers, you write `unsafe impl Send for MyType {}`, and the `unsafe` keyword *is* the admission: a behavioural obligation exists here that no compiler can discharge, so a human signs for it. Safe traits carry documented obligations too — an `Ord` implementation that is not a total order makes sorting produce nonsense or panic — but violating those is merely wrong, not undefined, so no keyword guards them. ## What you do when nothing checks it On a mainstream stack the honest answer is: mechanise it in tests, not in types. Write one *conformance suite* per supertype — the behavioural laws stated as executable tests over a factory — and run it against every implementer, including decorators and mocks; this is what a TCK is. Property-based testing raises the coverage of that suite from examples to generated inputs. External checkers (pluggable qualifier systems such as the Checker Framework) bolt on properties the language never had. And you narrow the promise itself: an interface that promises less has less to break. ## Interview shape One sentence that the type checker verifies shape and nothing else. Then the spread: Go and TypeScript infer the claim from a shape nobody declared; Java and C# accept covariant arrays and repair them on every store; SPARK and OpenJML actually prove inherited contracts at build time, at a specification cost; Rust's `unsafe impl` names the unverifiable obligation out loud. Close on the shared conformance suite as what you run where none of that exists.
- In Go and TypeScript a matching shape is the whole claim. How do you get a deliberate, nominal claim back when you need one?In Go, put an unexported method in the interface: only types declared in your package can satisfy it, which seals the interface and makes conformance intentional. In TypeScript, brand the type with a private class field or a `unique symbol` property that foreign object literals cannot produce. Neither checks behaviour — they only ensure a human deliberately opted in, which is the difference between an asserted claim and an accidental one.
- Why did Java and C# make arrays covariant if it costs a type check on every store?Both predate generics, and covariant arrays were the only way to write a routine that worked over an array of anything — sorting, copying, printing. The designers traded soundness for expressiveness and paid for it with a dynamic store check. Generics later gave a sound alternative, but the array behaviour stayed; JIT compilers now elide most of the checks when the element type is provably fixed.
- A shared conformance suite is the usual fallback. What can it actually establish, and what can it never catch?It establishes that each implementer satisfies the supertype's stated laws on the inputs the suite exercises, and property-based generation widens that from examples toward the input space. It cannot prove anything: it samples paths, so promises about timing, resource use, failure modes under load, or concurrency interleavings escape it unless you state them as testable properties. It is evidence, not verification — which is exactly the gap SPARK and OpenJML try to close by proof.
A building inspector who measures every doorway to confirm it is a standard size, and never once opens a door. The paperwork passes; whether the door swings is somebody else's problem.
saying these in an interview costs you the question
- "If it compiles, substitution is safe" — the compiler has checked member shape, not behaviour.
- Believing Go or TypeScript verifies that a type is a genuine implementer; both accept any matching shape, including an accidental one.
- Claiming Java array covariance is caught at compile time, or that generics fixed the array behaviour — the arrays still throw on store.
- Reading Rust's `unsafe impl Send` as "the code inside is unsafe" or "it disables the borrow checker", rather than "the compiler cannot verify this obligation, so the author vouches for it".
- Claiming runtime assertion checks prove the claim; they only observe the paths that actually execute during a run.