skip to content

Two loaded types have identical names, yet a run-time type test between them fails — what makes them different types?

level: seniorimportance: nice to knowfreq 33%

answer

  1. the name is only a label
  2. identity includes who defined it
  3. same definition, two definers, two types
  4. the diagnostic names the same type twice
  5. cross the boundary with data, not instances

basics

~20 s

A type's run-time identity is not its name. It is the name together with whatever defined it, so the same definition installed twice produces two distinct types, and a value of one is not assignable to the other.

solid answer

~40 s

The name is a label; the identity is the label plus the definer that installed the type. When a system defines the same type twice — two isolated component boundaries, a reloaded definition, the same dependency supplied on both sides of a partition — the result is two types, not one shared type. The symptom is confusing precisely because the names match: a type test says no, an assignment is refused, and the diagnostic names the same type on both sides of the complaint. The rule for a discovery-driven component is to compare types by identity and never by comparing name strings, and to pass data across such a boundary as a neutral shape the far side rebuilds, rather than as an instance of a type each side defines for itself.

code

pseudocode · 6 lines
pseudocode
handlerType = typeExpectedBySlot              // installed by one definer
recordType  = describeType(record).type       // produced by another definer

nameOf(handlerType) == nameOf(recordType)     // true  - same label
handlerType is recordType                     // false - different identities
assignableFrom(handlerType, recordType)       // false - the type test refuses

go deeper

for a junior

Take away one fact: a type's name is not the same thing as its identity, so two types can share a name and still be different types to the runtime.

for a middle

Explain how a process comes to hold two copies of one type and why a value of one is refused where the other is expected, in terms of identity rather than names.

for a senior

Recognise the symptom from the diagnostic that names the same type twice, confirm it by comparing identities and definers, and fix it by publishing the shared contract once or moving data instead of instances.

for a principal

Own the boundary design: isolation gives you independent component lifecycles and takes away the shared type, so decide up front which types are published once above the boundary and make everything else cross as data.

## Name is a label, identity is name plus definer A loaded type is identified at run time by more than its name. Two things determine it: the name, and the thing that defined it — the component that read the definition and installed it into the runtime. Change either and you have a different type. The name alone is just how humans and diagnostics refer to it. This is easy to forget because in a simple program it never comes up. One definer, one definition of each name, so name and identity agree perfectly and a name comparison would work. The distinction only becomes visible once a process contains more than one definer. ## What the walker sees | observation | what is really happening | |---|---| | a type test refuses an object whose type name matches | two identities, one name | | an assignment is refused and names the same type twice | the diagnostic prints names, and both names are equal | | a description looks right but a cast fails | the description is of the *other* copy of the type | | a handler table misses, keyed by name | two records now collide or shadow under one key | The second row is what makes the bug memorable. A message that says a value of one type cannot be treated as that same type reads like nonsense until you know that identity is not the name. ## Why a process ends up with two copies - **Isolation by design.** A component boundary that keeps each component's types separate is doing exactly what it was built to do; two components each carrying the same dependency get two copies of its types. - **Reloading.** Installing a new definition of a type does not remove the old one while objects created from it are still reachable; both exist, and instances made before the reload keep the old identity. - **Duplicate supply.** The same definition reaches the process by two routes — bundled inside one component and provided by the surrounding environment as well. None of these is a malfunction. They are the price of the isolation that makes plugin and modular layouts work at all. ## What still crosses the boundary and what does not 1. **A type defined once, above both sides, crosses fine.** If both sides obtained the contract from the same definer, there is one identity, and instances satisfy it on both sides. 2. **An instance of a type each side defines does not cross.** It will be refused at the first type test, no matter how identical the two definitions look. 3. **Neutral data crosses.** Text, numbers, and records of those, reconstructed into a local type on the far side, are not subject to the problem because no type identity travels with them. That is the whole design rule for a discovery-driven component that must work across such a boundary: publish the contract from a single place, or move data rather than instances. ## Rules for a component that walks types - **Compare types by identity**, using the runtime's own equality or assignability test, never by comparing name strings. A name comparison is the bug wearing the costume of a fix: it will happily match two unrelated types. - **Resolve a type through the same definer as the object you are holding**, not through whatever definer your own code happened to load from — otherwise you get the right name and the wrong type. - **If you key anything by type, key it by the type itself.** A table keyed by name silently serves one copy's entry to the other copy, which is a correctness problem and not just a missed lookup. - **Do not infer from a failed test that the object is corrupt.** The object is fine; the expectation about which type it is was wrong. ## Confirming the diagnosis Ask each side for the identity of the type it holds and compare those identities directly, then ask each type which definer produced it. Equal names with different definers confirms the diagnosis immediately and separates it from every other cause of a refused cast — a genuinely unrelated type, a value that really is of the wrong type, or a test written against the wrong contract. How a runtime decides which definer serves which request is that runtime's own machinery, and different runtimes answer it differently; what this leaf owns is the consequence a self-describing program must live with: the type a description belongs to is a specific loaded type, not every type that happens to share its name.

  • The diagnostic names the same type on both sides — how do you confirm what is going on?
    Ask each side for the identity of the type it is holding and compare those identities rather than the printed names, then ask each type which definer produced it. Two different definers behind one name confirms it. That check also rules out the mundane causes — a genuinely different type, a value that really is wrong, a test written against the wrong contract — which print differently.
  • What kind of value can safely cross a boundary where both sides define the same type?
    One whose type has a single identity both sides share, meaning it was defined once above the boundary and both sides obtained it from there. Otherwise, move data instead of instances: a neutral shape of text and numbers that the far side rebuilds into its own local type. Passing the description across does not help either, since it describes one specific loaded type.

saying these in an interview costs you the question

  • Says two types with equal names must be the same type.
  • Compares types by comparing their name strings.
  • Concludes from the failed test that the object is corrupt.
  • Expects an instance to cross an isolation boundary unchanged.
  • Assumes reloading a definition replaces the old type everywhere.