Skip to content

Appendix a1 — Catalog of special types

This appendix is a reference catalog gathering, in one place for cross-volume lookup, the “special types” introduced here and there in the main volume — untyped (Dynamic[Top]), void, never (Bot), and the two ends of the lattice Top / Bot. It assumes readers who came here via a “for details see appendix a1” pointer from Little Part 1 (untyped’s first appearance), Part 4 (Union and the bottom type), Part 5 (a branch going empty under narrowing), Part 8 (the return type void), and Part 9 (the sum-up box of the three special types). Look up only the items you need, and return to the main volume. Where each type is dug into deeper is signposted in “Going further” at the end. The main line sticks to organizing concepts and produces no new code (only the chapter-end notes occasionally attach a tiny-version demo).


a1-1. untyped (= Dynamic[Top]) — the heart of gradual

Section titled “a1-1. untyped (= Dynamic[Top]) — the heart of gradual”

untyped is the type representing “unknown / no way to check — keep quiet.” In the chibirigor body it’s the Dynamic type carrier, and to_s prints untyped. It’s what type_of returns when it loses the type (unknown syntax, unknown method), and where untyped appears = where Rigor lost the type.

What’s often confused about untyped (and TypeScript’s any) is that two originally separate axes look like one.

  • Axis A: position in the subtyping lattice — the top of the type lattice (Top / ⊤) or the bottom.
  • Axis B: checked or not — a sound static type, or gradual’s “stay quiet” type.

Laid out on these two axes, the differences among similar faces become clear:

  • TypeScript’s unknown is a plain top type (axis A only). Anything can be assigned to it, but you can do nothing with it until you narrow it. A one-way street, “can be put on top but not taken out from below,” and therefore sound.
  • TypeScript’s any is not just “anything can be assigned to it” but “it can be assigned to anything.” It behaves as if it’s at the top and bottom of the lattice at once — a contradiction as a subtyping relation, so any is a switch that gives up soundness and turns checking off (axis B). This is the true form of gradual typing’s dynamic type ?.
  • Dynamic[Top] is the expression that deliberately decomposes these two and shows them by name. Top is the part (value set) of a sound top type “anything could be here,” and Dynamic is the gradual ? marker laid on top of it (the mark for “the type was lost. stay quiet, don’t frighten”). Its behavior is close to any, but it isn’t collapsed into one word — “where it stayed quiet” remains as structure. That Little Part 9’s rigor check --explain can map every fail-soft site is because this Dynamic marker doesn’t vanish.

In a phrase — unknown presses you with “you don’t know, so narrow it,” while any / Dynamic[Top] say “you don’t know, so stay quiet.” Same “don’t know,” but the attitude is exactly opposite — take soundness, or take not-stopping the working code.

Cross-language / cross-tool correspondence

Section titled “Cross-language / cross-tool correspondence”

untyped isn’t a Ruby-only concept; any language with a type checker necessarily has a counterpart. Only the name differs; the role is always “unknown / no way to check — keep quiet.”

Language / toolNameIn a phrase
TypeScriptany / unknownany turns all checking off; unknown requires narrowing
Python (mypy)Anyflows in both argument and return directions
Gointerface{} / anythe empty set of an interface = anything fits
PHP (PHPStan)mixeda root type and also the mark of “unknown”
C#dynamicturns compile-time checking off
Elixir (Dialyzer)dynamic()the “universe” of set-theoretic types
Rigor / RBSuntyped (internal: Dynamic[Top])Top (the supremum of all types) with Dynamic laid on it

a1-2. void — “a value is returned but don’t look at it,” akin to ⊤

Section titled “a1-2. void — “a value is returned but don’t look at it,” akin to ⊤”

void is a type exclusive to RBS’s return position, meaning “a value is returned, but don’t rely on (don’t look at) that value.” It’s put on methods “called for a side effect,” like Array#each.

In RBS’s type system, void’s lattice behavior is nearly the same as the top type (⊤) (top-like) — because “it accepts any value / you can extract nothing from that value.” But void is a return-position-only marker, and when it appears in a value position it’s treated as folded back to top (it’s not perfectly identical to the top type). What differs is only the nuance (the message put into it):

  • The top type / unknown says “there’s a meaningful value here. narrow it before you use it.”
  • void says “there’s a value here but it has no meaning. don’t use it at all.

From the same lattice position, it sends the reader the opposite signal — a fine example of the type system being identical but the intent differing.

A Ruby method’s last evaluated expression is its implicit return value, so a method tagged void does return something at run time, and rewriting the implementation can change that value. void is the declaration that honestly admits this reality as a type, tantamount to saying “this method could return any value.” That’s exactly why it works on the contract side:

  • Return type void makes the caller promise “don’t depend on the return value” ⇒ changing the return value later isn’t a breaking change (BC break).
  • Conversely, declaring -> nil means promising “I return nil,” and later changing it to return another value breaks the contract.

Herein lies the practical benefit of choosing void.

void works on the checking side (a bridge to Seasoned Part 1)

Section titled “void works on the checking side ⇐ (a bridge to Seasoned Part 1)”

chibirigor is on the side that synthesizes return types from the body (it builds types, not verifies annotations), so void doesn’t appear — it always emits a concrete type, “the type of the last expression.” That is, void doesn’t appear on the synthesis side that assembles a type; it works on the checking side that checks an RBS-declared return type against the body. The true form of that (subsumption / the three-valued acceptance check) is treated in Seasoned Part 1.


a1-3. never (= Bot) — the type of an unreachable branch

Section titled “a1-3. never (= Bot) — the type of an unreachable branch”

If the top type is the lattice’s ceiling “anything could be here,” its exact opposite is the bottom type (Bot, ⊥). “The smallest box, into which not a single value goes” — the type representing unreachable / can’t happen.

  • Non-returning expressions: a path that only raises and returns no value; an infinite loop.
  • A branch narrowed dry: the dead branch of Little Part 5. In the then-branch of applying if x.is_a?(String) to x : Integer, if x had a type it would be “Integer and String” — no such value exists, so the type is the empty set = bottom. You could also call it the type of “narrowed until zero candidates remain.”

Its property, too, is dual to top.

  • A top-type value “could be anything, so you can do nothing until you narrow it.
  • A bottom-type value “can’t be obtained in the first place, so if you assume it were obtained, you can use it for anything” — this is Bot <: T: it’s a subtype of every type. “A function that returns never” = “a function that never returns” is this restatement.

Cross-language / cross-tool correspondence

Section titled “Cross-language / cross-tool correspondence”
Language / toolNameWhere it appears
TypeScriptnevergaps in exhaustiveness checks; the return of throw/infinite loops; a branch narrowed dry
Rust! (never type)panic!, return, loop {} and other “non-returning” expressions
Scala / KotlinNothingan expression that only throws; the element type of an empty collection
HaskellVoida type with no value (zero inhabitants)
Rigor / RBSbot (internal: Bot)unreachable; a path that always stops at raise

a1-3x. A note: chibirigor has an unreachable-arm diagnostic too (opt-in)

Section titled “a1-3x. A note: chibirigor has an unreachable-arm diagnostic too (opt-in)”

In line with the policy above, we added unreachable detection to chibirigor too. Without building a Bot type carrier, it directly judges “the branch’s subject becomes the empty set (= Bot)” and emits an :info diagnostic (unreachable_branch? in lib/chibirigor/narrowing.rb). You opt in with check --unreachable:

Terminal window
$ printf 'x = 1\nif x.is_a?(String)\n y = x + 1\nelse\n z = x * 2\nend\n' > demo.rb
$ ruby exe/chibirigor check --unreachable demo.rb
demo.rb:3:3: info: this branch is unreachable (the condition is always false)
y = x + 1
^^^^^^^^^

The then-branch of applying if x.is_a?(String) to x : Integer is “Integer and String” = zero inhabitants = Bot, so it’s unreachable. By default (no flag) it stays quiet — so as not to break Little Part 4/5’s promise of “don’t narrow, don’t touch a dead branch (zero false positives).” Telling you “this won’t be taken” is reserved for when the reader explicitly asks (--unreachable).

For soundness, it asserts only when it can prove:

  • only when the subject is a closed known type (containing no untyped at all). If even a little Dynamic mixes in, it stays quiet (gradual).
  • is_a? asserts “can’t happen” only between concrete classes (leaves) it can be sure are mutually disjoint. An ancestor relation like is_a?(Numeric) or is_a?(Object) it doesn’t assert (it holds no ancestor table) — because even with x : Integer, x.is_a?(Numeric) can be true (avoiding a false positive).

This is a miniature of real Rigor’s ADR-47 (flow.unreachable-clause). The real thing judges a clause where narrowing narrowed the subject to bot as dead, and has an FP envelope (a fence of exclusion conditions to prevent false positives) excluding loops, blocks, and gradual. chibirigor minimizes the same idea down to three points: “leaf-class disjointness,” “closed types only,” and “opt-in.”


a1-4. Top / Bot — the two ends of the lattice

Section titled “a1-4. Top / Bot — the two ends of the lattice”

The subtyping <: lattice has two ends. Bot <: T <: Top — every type T necessarily fits between Bot as the lower end and Top as the upper end.

Top (⊤) ← every type is a subtype of this (the biggest box)
╱ │ ╲
{name:} Integer String ← concrete types
╲ │ ╱
Bot (⊥) ← a subtype of every type (unreachable; the smallest box)

The two ends are dual:

Top (⊤)Bot (⊥)
Value setanything fits (the biggest box)nothing fits (the smallest box)
Subtypingevery type is a subtype of Top (T <: Top)a subtype of every type (Bot <: T)
Using the valuecan do nothing until you narrow itcan’t be obtained in the first place = usable for anything
Face in the main volumeunknown / void (Little Part 1, 8)never (Little Part 4, 5)

Where subsumption is the operation of “loosening upward (toward Top),” Bot, from the lattice’s floor, “can impersonate any type” — the directions being symmetric is the lattice’s beauty.


a1-5. The three types compared, summed up (untyped / void / never)

Section titled “a1-5. The three types compared, summed up (untyped / void / never)”

Laying out the three beginners most often confuse makes the axes clear.

TypeControl returns?The value?In a phraseFirst appears
untyped (unknown)returnsexists but type unknown”the contents are unknown”Little Part 1
voidreturnsexists but don’t look”don’t rely on the return value”Little Part 8
never (Bot)doesn’t returnnone (zero inhabitants)“doesn’t come back at all”Little Part 4
  • void is neither “the value’s contents are unknown” (untyped) nor “doesn’t come back” (never); it’s a ‘be quiet’ mark standing in the return position — a relative of untyped in telling the checker “you needn’t check the return value here” (but its target is not a value but how the return value is used).
  • untyped and void are both family of “be quiet,” but untyped doesn’t stand on its own as a top in the lattice (a gradual type with a marker on Top), while void is akin to top on the lattice (but a return-position-only marker).
  • Only never / Bot is at the floor of the lattice, opposite in direction from the other two (the ceiling side).

a1-6. Going further (where each deeper dive lives)

Section titled “a1-6. Going further (where each deeper dive lives)”

This appendix is an organization for “looking up.” The deeper dives into the theory live here:

  • never / Bot, the formal treatment of the lattice and duals → Seasoned Part 2 “Subtyping and variance” (Bot <: T <: Top, S-Arrow and variance).
  • void / Top, checking a return type → Seasoned Part 1 “Bidirectional typing” (the map of synthesis / checking ).
  • untyped’s gradual consistency ~ (symmetric, non-transitive) → Seasoned Part 2 §2-5, and the sum-up of “unsound on purpose” is Seasoned Part 7.
  • refinement carriers (non-empty-string, positive-int, etc., types narrowed by a predicate) → appendix a2 “Narrowing patterns” / the glossary’s “refinement carrier” and “Difference type” entries. The difference from Const (a pinpoint value) is there too.
  • The sum-up in the main volume → Little Part 9’s “three special types” sum-up box.

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.