A runtime type test -- Python's isinstance, a Go type assertion on an interface, JavaScript's instanceof -- appears to ask the same question of an object. What is each one actually checking, and where do they give different answers for objects of the same shape?
answer
- three relations: declared, structural, registered
- Python __instancecheck__ + abc.register lies about inheritance
- Go checks method sets, consumer-side interfaces
- JS instanceof = one prototype object identity, cross-realm false
- TypeScript erased -> hand-written guards
basics
~20 sThey ask three different questions. Python's isinstance asks declared-or-registered membership, and a metaclass hook can make it say yes without inheritance. Go asks whether the concrete type's method set covers the interface, with no declaration anywhere. JavaScript's instanceof asks whether one specific constructor's prototype object is on the chain, so it fails across realms.
solid answer
~50 sA type test is membership in a relation, and languages disagree about which relation. - **Python** -- nominal by default, but `isinstance` is dispatched through the metaclass's `__instancecheck__`. A class passed to `abc.ABCMeta.register` answers `True` while inheriting nothing, so `isinstance` and the MRO disagree: `super()` and attribute lookup see no relationship at all. - **Go** -- an assertion `v, ok := x.(io.Reader)` checks that the dynamic type's **method set** contains `Read`. A type satisfies it having never named `io.Reader`, and satisfaction can be gained accidentally by adding a method. - **JavaScript** -- `instanceof` compares against **one particular constructor's** prototype object. An array from an iframe or a Node `vm` context fails `arr instanceof Array` in the host realm, which is precisely why `Array.isArray` exists. - **TypeScript** -- its structural types are erased, so there is nothing to test; you hand-write predicates. - **Rust** -- no subtype test at all; `dyn Any` downcasts to one exact concrete type. So "is-a" splits into declared ancestry, registered conformance, and structural capability.
code
go · 6 linestype Counter struct{ n int }
func (c *Counter) Read(p []byte) (int, error) { return 0, nil }
var x any = &Counter{}
_, ok := x.(io.Reader) // ok == true, no declaration anywherego deeper
Know that a runtime type test asks whether an object belongs to a type, and that different languages answer using different information.
Name the three relations -- declared ancestry, structural method sets, registered conformance -- and give one language for each.
Diagnose the real failures: cross-realm instanceof, accidental interface satisfaction in Go, an isinstance that passes while the method is missing; and know the standard workaround for each.
Argue about where type tests belong at all: closed sums at boundaries versus dispatch inside a domain, and how the choice of relation shapes API extensibility -- structural satisfaction lets you adapt types you do not own, nominal declaration lets you control who may claim conformance.
## What a type test is, formally Asking "is this object an X?" is asking whether the object's type belongs to a set. Languages define that set three different ways, and the definitions do not coincide. 1. **Declared ancestry (nominal).** X is a node in a hierarchy the programmer wrote down; membership is reachability in that declared partial order. This is the model most people assume by default. 2. **Structural capability.** X is a predicate over the type's signature -- "has these methods with these types". Membership is computed, not declared, and any type that happens to satisfy the predicate is in the set forever, including types written before X existed. 3. **Registered conformance.** X keeps a side table of types that count as members regardless of where they sit in the hierarchy. Membership is neither declared in the type nor derived from its shape; it is asserted by a third party. ## Python: nominal, until the metaclass says otherwise `isinstance(obj, C)` normally checks `type(obj)`'s MRO. But the check is a protocol: `type(C).__instancecheck__(C, obj)` runs first. `abc.ABCMeta` implements it against a registry, so `MyBuffer` can be registered with an abstract base and answer `True` to `isinstance` while having no inheritance link whatsoever. Nothing else changes -- attribute lookup, `super()`, and the MRO are untouched, and none of the abstract base's default methods are inherited. This is the sharpest illustration that a type test's answer and the object's actual capabilities are separate facts. ## Go: structural, checked at runtime over method sets Go has interfaces but no `implements`. A type satisfies `io.Reader` when it has `Read([]byte) (int, error)`, and the assertion `x.(io.Reader)` checks that at runtime against the value's dynamic type descriptor. Two consequences follow. First, a type can satisfy an interface defined in a package it has never heard of, which is why Go code declares interfaces at the *consumer*. Second, satisfaction can be gained by accident: add a method with the right name and signature for unrelated reasons, and the type silently becomes a member of every interface that predicate matches. Pointer receivers add a wrinkle -- the method set of `T` and of `*T` differ, so the same underlying type answers differently depending on whether you hold a value or a pointer. ## JavaScript: identity of one constructor's prototype object `x instanceof C` walks x's prototype chain looking for the exact object `C.prototype`. It is an identity comparison, not a shape or name comparison. Load the same class twice -- an iframe, a Node `vm` context, two copies of a library in a dependency tree -- and you get two distinct `C.prototype` objects, so an array built in one realm is not `instanceof Array` in the other even though it is unquestionably an array. The standard library's answer is `Array.isArray`, which asks the internal slot instead; `Symbol.hasInstance` lets a class override `instanceof` outright, making the test whatever the author wants. ## TypeScript and Rust: nothing to test TypeScript's types are structural but exist only at compile time, so at runtime nothing remains to interrogate -- hence user-defined type guards (`function isDuck(x): x is Duck`) that hand-write the predicate the compiler already knew. Rust has no subtyping over traits and no general test; `dyn Any::downcast_ref::<T>()` compares one concrete `TypeId` for equality, deliberately offering identity rather than membership. ## Why this matters in design Every type test is a place where your program branches on identity rather than on behaviour, and the three relations fail in different ways. A nominal test rejects a perfectly capable adapter that forgot to declare the interface. A structural test accepts a type that has the right method by coincidence and the wrong meaning. A constructor-identity test fails on values that crossed a boundary. That is why the durable advice is to test capability, or better, to dispatch rather than test at all -- but you cannot even state which failure you are exposed to until you know which relation your language's operator implements.
- In Go, why do teams define an interface next to the code that consumes it rather than next to the type that satisfies it?Because satisfaction is structural and checked at use, the implementing type needs no knowledge of the interface at all. Declaring the interface at the consumer keeps it as narrow as that consumer's actual needs, usually one or two methods, which makes test doubles trivial and avoids a shared package that every implementation must import. The provider side would otherwise guess at a wide interface nobody needs in full.
- Python's abc.register makes isinstance return True without inheritance. What does that break?It splits the type test from the object's real capabilities. The registered class inherits none of the abstract base's concrete default methods and does not appear in the MRO, so super() and cooperative multiple inheritance see nothing. Code that trusts the isinstance check and then calls a method the base merely declared will fail at the call, not at the check.
- Given the failure modes of all three, when is a runtime type test still the right tool?When you are at a boundary that genuinely receives values of unrelated types -- deserialization, plugin loading, a variant or sum type being narrowed -- and the set of alternatives is closed and known. Inside your own hierarchy, a type test is usually a dispatch you failed to model, and the fix is a method on the type or a visitor rather than a chain of tests.
saying these in an interview costs you the question
- Saying all three operators "check the class" -- only one of them checks declared ancestry.
- Believing JavaScript's instanceof compares by class name or shape; it compares one prototype object by identity.
- Assuming a Go type must declare the interface it satisfies.
- Treating Python's isinstance result as proof the object inherited the base's implementations.
- Claiming TypeScript's structural types can be tested at runtime.