Generics & Types — Find the Bug¶
12 buggy snippets where a type-system misuse produces a real runtime failure — an NPE-equivalent, a
ClassCastException, anArrayStoreException, a silently-missed union variant, garbage data slipping past a domain type. In every case the code compiles and the type checker is green. The type system was lied to, widened, or subverted, and the lie surfaced at runtime. Find where the type and the reality diverge.
Table of Contents¶
- Snippet 1 —
ascast lies about a shape (TypeScript) - Snippet 2 —
JSON.parsetyped as a domain type (TypeScript) - Snippet 3 — Unchecked cast / raw type →
ClassCastException(Java) - Snippet 4 — Array covariance →
ArrayStoreException(Java) - Snippet 5 — Non-exhaustive switch silently drops a variant (TypeScript)
- Snippet 6 — Type hint that lies via an
Anyleak (Python) - Snippet 7 —
interface{}round-trip loses the type (Go) - Snippet 8 —
Numberwidening / boxing identity (Java) - Snippet 9 — Generic method violates an unstated invariant (TypeScript)
- Snippet 10 — Optional chaining hides a missing field after a cast (TypeScript)
- Snippet 11 —
==identity vs structural equality (Go / Java) - Snippet 12 —
TypedDict/castletsstrmasquerade asint(Python)
How to Use¶
For each snippet:
- Read the code. Assume it compiles cleanly —
tsc --strictis green,javacemits at most a warning,mypy --strictpasses,go buildsucceeds. - Ask: where does the static type disagree with the runtime value? The type checker believed a claim the code never proved.
- Predict the failure:
undefined/NPE, wrong branch taken,ClassCastException,ArrayStoreException, silent corruption, or a metric that lies. - Open the answer. It names the bug, explains why the type system did not catch it (where the type was widened, asserted, or erased), and gives the fix that re-grounds the type in reality.
The recurring lesson: a type is a promise. as, any, raw types, interface{}, unvalidated JSON.parse, and unchecked casts all let you make a promise you never keep. The compiler trusts you; the runtime does not.
Snippet 1 — as cast lies about a shape (TypeScript)¶
Difficulty: ⭐ Warm-up
interface User {
id: string;
name: string;
profile: { avatarUrl: string };
}
function getUser(id: string): User {
// The API actually returns { id, name } — `profile` is on a separate endpoint.
const raw = fetchSync(`/users/${id}`); // returns { id: string; name: string }
return raw as User;
}
function renderAvatar(id: string): string {
const user = getUser(id);
return `<img src="${user.profile.avatarUrl}" alt="${user.name}">`;
}
What's wrong?
Answer
`getUser` claims its return type is `User`, but the API response has no `profile` field. The `as User` cast tells the compiler "trust me, this is a `User`" — so `tsc` never complains that `profile` is missing. At runtime, `user.profile` is `undefined`, and `user.profile.avatarUrl` throws: This is the TypeScript NPE-equivalent. The crash happens in `renderAvatar`, two functions away from the lie in `getUser`. **Why the type system didn't catch it:** `as` is a *type assertion*, not a *type conversion*. It performs zero runtime work and zero structural checking — it simply overrides the compiler's knowledge. `raw` was `{ id, name }`, but `as User` erased that and substituted the richer type. The compiler had the correct information (`raw` was narrower than `User`) and was forced to discard it. **The fix:** never `as` across a structural gap. Either parse-and-validate at the boundary, or model the truth.import { z } from "zod";
const UserApiSchema = z.object({ id: z.string(), name: z.string() });
// The API's true shape — no profile.
type UserApi = z.infer<typeof UserApiSchema>;
function getUser(id: string): UserApi {
const raw = fetchSync(`/users/${id}`);
return UserApiSchema.parse(raw); // throws here, at the boundary, if the shape is wrong
}
async function renderAvatar(id: string): Promise<string> {
const user = getUser(id);
const profile = await fetchProfile(id); // profile is a separate concern — model it as one
return `<img src="${profile.avatarUrl}" alt="${user.name}">`;
}
Snippet 2 — JSON.parse typed as a domain type (TypeScript)¶
Difficulty: ⭐⭐ Common in the wild
interface Order {
id: string;
total: number; // dollars
status: "pending" | "paid" | "shipped";
createdAt: Date;
}
function loadOrder(json: string): Order {
return JSON.parse(json) as Order;
}
function isOverdue(order: Order): boolean {
const ageMs = Date.now() - order.createdAt.getTime();
return order.status === "pending" && ageMs > 7 * 24 * 60 * 60 * 1000;
}
const order = loadOrder('{"id":"A1","total":"49.99","status":"PAID","createdAt":"2026-01-01T00:00:00Z"}');
console.log(isOverdue(order));
What's wrong?
Answer
`JSON.parse` returns `any` by design — JSON has no notion of `Date`, of string-literal unions, or of "this number must not be a string". The `as Order` slaps a domain type onto raw deserialized data without checking anything. **Three lies** ride in on that cast: 1. **`createdAt` is a `string`, not a `Date`.** JSON has no date type. At runtime `order.createdAt.getTime` is `undefined`, so `order.createdAt.getTime()` throws `TypeError: order.createdAt.getTime is not a function`. 2. **`total` is the string `"49.99"`, not the number `49.99`.** Any arithmetic (`total * 1.08` for tax) would do string coercion — `"49.99" * 1.08` happens to coerce, but `total + shipping` would concatenate (`"49.995.00"`). 3. **`status` is `"PAID"`, not one of `"pending" | "paid" | "shipped"`.** The literal-union type is violated; the `order.status === "pending"` check silently never matches a `"PAID"` order, so it is treated as not-pending. `tsc` is green because `as` suppressed all of it. **Why the type system didn't catch it:** TypeScript types are erased before runtime and `JSON.parse`'s return is `any`. The compiler has *no* information about the parsed bytes — `as Order` is the programmer asserting knowledge they do not have. The domain type (`Date`, the literal union) describes an *in-memory* model that the wire format does not satisfy. **The fix:** validate and transform at the deserialization boundary. A schema library does the parsing *and* the coercion:import { z } from "zod";
const OrderSchema = z.object({
id: z.string(),
total: z.number(), // rejects "49.99" (string)
status: z.enum(["pending", "paid", "shipped"]), // rejects "PAID"
createdAt: z.coerce.date(), // ISO string -> Date
});
function loadOrder(json: string): Order {
return OrderSchema.parse(JSON.parse(json)); // throws on the bad fixture above
}
Snippet 3 — Unchecked cast / raw type → ClassCastException (Java)¶
Difficulty: ⭐⭐ Common in the wild
import java.util.*;
public class Cache {
private final Map store = new HashMap(); // raw type
public void put(String key, Object value) {
store.put(key, value);
}
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) store.get(key); // unchecked cast
}
}
// Caller:
Cache cache = new Cache();
cache.put("retries", 3); // an Integer
int retries = cache.<Integer>get("retries");
cache.put("name", "service-a"); // a String
long timeout = cache.<Long>get("name"); // (!) expecting a Long
What's wrong?
Answer
`get` is declared `public final class TypedKey<T> {
private final String name;
private final Class<T> type;
public TypedKey(String name, Class<T> type) { this.name = name; this.type = type; }
String name() { return name; }
Class<T> type() { return type; }
}
public class Cache {
private final Map<String, Object> store = new HashMap<>();
public <T> void put(TypedKey<T> key, T value) {
store.put(key.name(), value);
}
public <T> T get(TypedKey<T> key) {
return key.type().cast(store.get(key.name())); // checked cast — throws at put-time mismatch impossible
}
}
static final TypedKey<Integer> RETRIES = new TypedKey<>("retries", Integer.class);
// cache.put(RETRIES, "oops") -> compile error: String is not Integer
Snippet 4 — Array covariance → ArrayStoreException (Java)¶
Difficulty: ⭐⭐⭐ Subtle
public class Inventory {
static void fillWithDefault(Object[] slots) {
for (int i = 0; i < slots.length; i++) {
slots[i] = "EMPTY"; // a String as the default
}
}
public static void main(String[] args) {
Integer[] partNumbers = new Integer[3];
fillWithDefault(partNumbers); // compiles fine
System.out.println(partNumbers[0]);
}
}
What's wrong?
Answer
Java arrays are **covariant**: `Integer[]` is a subtype of `Object[]`, so passing an `Integer[]` where `Object[]` is expected compiles without complaint. Inside `fillWithDefault`, the static type is `Object[]`, so writing a `String` into it type-checks. But the *runtime* array is still an `Integer[]`. The JVM checks every array store against the array's actual component type, and: is thrown at `slots[i] = "EMPTY"`. **Why the type system didn't catch it:** covariant arrays are a known unsoundness in Java's type system, intentionally traded for expressiveness in the pre-generics era. The compiler accepts the covariant widening (`Integer[]` → `Object[]`), so the write *looks* legal statically. Soundness is recovered only by a runtime store check — which is exactly the `ArrayStoreException`. The type system "didn't catch it" because it deliberately defers this category of error to runtime. **The fix:** use generic collections, which are *invariant* (`ListSnippet 5 — Non-exhaustive switch silently drops a variant (TypeScript)¶
Difficulty: ⭐⭐ Common in the wild
type PaymentEvent =
| { kind: "authorized"; amount: number }
| { kind: "captured"; amount: number }
| { kind: "refunded"; amount: number };
// Six months later a teammate adds:
// | { kind: "disputed"; amount: number; reason: string };
function netRevenue(events: PaymentEvent[]): number {
let total = 0;
for (const e of events) {
switch (e.kind) {
case "authorized":
break; // not yet money
case "captured":
total += e.amount;
break;
case "refunded":
total -= e.amount;
break;
}
}
return total;
}
What's wrong?
Answer
When `"disputed"` is added to the `PaymentEvent` union, this `switch` has no case for it. A disputed payment is money that left the account (a chargeback), so it should reduce revenue — but it falls through every `case` and contributes `0`. **Revenue is silently overstated** by the sum of all disputes. No crash, no error — just wrong numbers in a financial report. **Why the type system didn't catch it:** a `switch` with no `default` and no exhaustiveness guard is *not* required to handle every union member. TypeScript happily compiles a partial `switch`; the missing variant is invisible. The type checker would only help if the code *forced* exhaustiveness via the `never` trick. **The fix:** add an exhaustiveness check in `default` so adding a variant breaks the build:function assertNever(x: never): never {
throw new Error(`Unhandled payment event: ${JSON.stringify(x)}`);
}
function netRevenue(events: PaymentEvent[]): number {
let total = 0;
for (const e of events) {
switch (e.kind) {
case "authorized":
break;
case "captured":
total += e.amount;
break;
case "refunded":
total -= e.amount;
break;
default:
return assertNever(e);
// ^ When "disputed" is added, `e` is `{ kind: "disputed"; ... }` here,
// which is NOT assignable to `never` -> COMPILE ERROR.
}
}
return total;
}
Snippet 6 — Type hint that lies via an Any leak (Python)¶
Difficulty: ⭐⭐⭐ Subtle
import json
from typing import Any
def load_config(path: str) -> dict[str, Any]:
with open(path) as f:
return json.load(f)
def get_max_connections(config: dict[str, Any]) -> int:
return config["max_connections"] # annotated int, but it's Any
def make_pool(config: dict[str, Any]) -> "ConnectionPool":
size: int = get_max_connections(config) # mypy: OK
return ConnectionPool(size=size)
# config.json contains: {"max_connections": "50"} <-- a string
pool = make_pool(load_config("config.json"))
# Later:
for _ in range(pool.size): # TypeError: 'str' object cannot be interpreted as an integer
...
What's wrong?
Answer
`get_max_connections` is annotated `-> int`, and `size: int = get_max_connections(config)` is annotated `int`. But `config` is `dict[str, Any]`, so `config["max_connections"]` has type `Any`. **`Any` is assignable to anything and everything is assignable to it** — so mypy accepts `return config["max_connections"]` as an `int` without proof, and accepts assigning it to `size: int`. The annotations *claim* `int`, but the value is whatever JSON produced — here the string `"50"`. `mypy --strict` is green. At runtime, `range(pool.size)` is `range("50")`: **Why the type system didn't catch it:** `Any` is a hole in the type system — it disables checking for any expression that touches it, and the "hole" propagates outward through return values and assignments (the `Any` leak). The `-> int` annotation is unbacked: nothing converts or verifies the value. mypy trusts the annotation precisely because the source is `Any`. **The fix:** validate at the boundary and *narrow* `Any` into a real `int` with an explicit, checked conversion: Better still, parse the whole config into a typed model so the leak is sealed at one point: Now `config.max_connections` is genuinely an `int`, and a non-numeric value fails loudly where the file is read.Snippet 7 — interface{} round-trip loses the type (Go)¶
Difficulty: ⭐⭐ Common in the wild
package main
import (
"encoding/json"
"fmt"
)
type Metric struct {
Name string
Value interface{} // accepts "any" numeric type
}
func total(metrics []Metric) int {
sum := 0
for _, m := range metrics {
sum += m.Value.(int) // type assertion
}
return sum
}
func main() {
data := `[{"Name":"hits","Value":10},{"Name":"misses","Value":5}]`
var metrics []Metric
_ = json.Unmarshal([]byte(data), &metrics)
fmt.Println(total(metrics))
}
What's wrong?
Answer
`Value` is `interface{}`, so it can hold anything — but `encoding/json` decodes every JSON number into a **`float64`**, never an `int`, when the target is `interface{}`. So after `Unmarshal`, `m.Value` holds `float64(10)`, not `int(10)`. The assertion `m.Value.(int)` then fails. In single-return form it **panics**: The code compiles cleanly — `interface{}` accepts any concrete type, and `.(int)` is a syntactically valid assertion. **Why the type system didn't catch it:** `interface{}` (now `any`) erases the static type entirely; the compiler has no idea what concrete type lives inside, so `m.Value.(int)` is accepted as a *runtime-checked* assertion. The mismatch between "what JSON produced" (`float64`) and "what the code assumed" (`int`) is exactly the kind of fact `interface{}` throws away. **The fix:** give the field a concrete type so unmarshalling and use agree, and the compiler does the work: If `Value` genuinely must be polymorphic, use the comma-ok assertion (never the single-return form on untrusted data) and handle every real case — including `float64`:Snippet 8 — Number widening / boxing identity (Java)¶
Difficulty: ⭐⭐⭐ Subtle
import java.util.*;
public class FeatureFlags {
private final Map<String, Long> rollout = new HashMap<>();
public void setPercent(String flag, long percent) {
rollout.put(flag, percent);
}
public boolean isFullyRolledOut(String flag) {
Long value = rollout.get(flag);
return value == 100; // compare to fully-on
}
}
// Caller:
FeatureFlags flags = new FeatureFlags();
flags.setPercent("new-checkout", 100);
System.out.println(flags.isFullyRolledOut("new-checkout")); // true or false?
flags.setPercent("dark-mode", 200); // clamp bug elsewhere, ignore
System.out.println(flags.isFullyRolledOut("dark-mode")); // ?
What's wrong?
Answer
`value` is a boxed `Long`; `100` is an `int` literal. The comparison `value == 100` triggers **unboxing**: `value.longValue() == 100`. For `"new-checkout"` that is `100L == 100`, which is `true` — *this one works*. The trap is more dangerous than it looks and bites in two ways: 1. **If anyone "optimizes" the comparison to `value == Long.valueOf(100)`** (comparing two `Long` objects), `==` becomes *reference* identity, not value equality. Java caches boxed values only in `[-128, 127]`, so `Long.valueOf(100) == Long.valueOf(100)` is `true` (cached) but `Long.valueOf(200) == Long.valueOf(200)` is **`false`** (two distinct objects). A flag set to a value above 127 would never compare equal to itself. 2. **If `rollout.get(flag)` returns `null`** (flag never set), `value == 100` unboxes `null` and throws `NullPointerException` — an NPE hidden inside an innocent-looking `==`. The `200` case in the caller is a landmine for variant (1) if the comparison is ever rewritten to box both sides — `200 > 127`, so identity comparison fails. **Why the type system didn't catch it:** `==` is overloaded by the *static* type of its operands. With a primitive on one side, it unboxes (value compare); with two boxed operands, it is reference compare. The type system happily accepts both, but they mean different things — and autoboxing makes the switch invisible. `null` is a legal value of every boxed type, so `Long value = ...; value == 100` type-checks even though unboxing `null` is fatal. **The fix:** never use `==` on boxed numbers; never let a boxed number be implicitly the comparison's "value" side without a null check.public boolean isFullyRolledOut(String flag) {
Long value = rollout.get(flag);
return value != null && value.longValue() == 100L; // explicit, null-safe, value compare
}
// or, avoid boxing entirely:
private final Map<String, Long> rollout = new HashMap<>();
public boolean isFullyRolledOut(String flag) {
return rollout.getOrDefault(flag, 0L) == 100L; // primitive long on the right forces value compare
}
Snippet 9 — Generic method violates an unstated invariant (TypeScript)¶
Difficulty: ⭐⭐⭐ Subtle
// "Merge a partial update into an entity."
function merge<T>(base: T, patch: Partial<T>): T {
return { ...base, ...patch };
}
interface Account {
id: string;
balance: number;
currency: "USD" | "EUR";
}
const account: Account = { id: "A1", balance: 100, currency: "USD" };
// A patch built from untyped form data:
const formPatch = { balance: undefined, currency: "GBP" } as Partial<Account>;
const updated = merge(account, formPatch);
console.log(updated.balance.toFixed(2)); // ?
What's wrong?
Answer
`merge` compiles and is "correct" for its signature — but `Partialfunction merge<T extends object>(base: T, patch: Partial<T>): T {
const defined = Object.fromEntries(
Object.entries(patch).filter(([, v]) => v !== undefined),
) as Partial<T>;
return { ...base, ...defined };
}
// And build the patch through validation, not `as`:
const PatchSchema = z.object({
balance: z.number().optional(),
currency: z.enum(["USD", "EUR"]).optional(),
});
const formPatch = PatchSchema.parse(rawFormData); // rejects "GBP" here
Snippet 10 — Optional chaining hides a missing field after a cast (TypeScript)¶
Difficulty: ⭐⭐ Common in the wild
interface ApiResponse {
data: {
user: {
settings: {
theme: "light" | "dark";
notificationsEnabled: boolean;
};
};
};
}
function shouldNotify(json: unknown): boolean {
const res = json as ApiResponse;
// Defensive optional chaining — surely safe?
return res?.data?.user?.settings?.notificationsEnabled ?? true;
}
// The real payload nests settings one level deeper: data.user.preferences.settings
const payload = { data: { user: { preferences: { settings: { notificationsEnabled: false } } } } };
console.log(shouldNotify(payload)); // ?
What's wrong?
Answer
The real payload nests `settings` under `preferences`, but the code (and the asserted `ApiResponse` type) expects it directly under `user`. So `res.data.user.settings` is `undefined`, optional chaining short-circuits, and `?? true` returns the **default `true`** — meaning the user is notified even though they set `notificationsEnabled: false`. The output is `true`. The user explicitly disabled notifications and gets them anyway. No crash; just a privacy/UX bug that looks like "defensive code working as intended." **Why the type system didn't catch it:** `json as ApiResponse` (again `as`) asserts a shape that does not match reality, so `tsc` validates the access path against the *fictional* type and approves `res.data.user.settings`. Optional chaining then masks the runtime mismatch: instead of throwing, it quietly yields `undefined`, and `??` substitutes a default. **Defensive `?.` + `??` on top of a lying `as` cast converts a structural mismatch into a silently-wrong default** — arguably worse than a crash, because nothing signals the error. **The fix:** parse the unknown input against the real schema, so a structural mismatch is a loud failure, not a silent default:const ResponseSchema = z.object({
data: z.object({
user: z.object({
preferences: z.object({
settings: z.object({
theme: z.enum(["light", "dark"]),
notificationsEnabled: z.boolean(),
}),
}),
}),
}),
});
function shouldNotify(json: unknown): boolean {
const res = ResponseSchema.parse(json); // throws if the shape is wrong
return res.data.user.preferences.settings.notificationsEnabled;
}
Snippet 11 — == identity vs structural equality (Go / Java)¶
Difficulty: ⭐⭐⭐ Subtle
package main
import "fmt"
type UserID struct {
Tenant string
Local string
}
func dedupe(ids []*UserID) []*UserID {
seen := map[*UserID]bool{} // keyed by pointer
out := []*UserID{}
for _, id := range ids {
if !seen[id] {
seen[id] = true
out = append(out, id)
}
}
return out
}
func main() {
a := &UserID{Tenant: "acme", Local: "42"}
b := &UserID{Tenant: "acme", Local: "42"} // same value, different pointer
fmt.Println(len(dedupe([]*UserID{a, b}))) // expected 1?
}
What's wrong?
Answer
`dedupe` keys the `seen` map by `*UserID` — the **pointer**, not the value. `a` and `b` describe the same user (`acme/42`) but are distinct allocations, so `a != b` as pointers and the map treats them as two different keys. `dedupe` returns **2**, not 1 — duplicate users survive. The author conflated "same identity" (pointer equality) with "same value" (structural equality). Comparing pointers asks *"are these the same object in memory?"*; the dedupe intent is *"do these represent the same user?"*. **Why the type system didn't catch it:** `*UserID` is a perfectly valid, *comparable* map key — Go allows pointer keys, and `==` on pointers is meaningful (identity). The type system has no way to know that the program *meant* value equality; both interpretations are well-typed. The bug lives entirely in the gap between "comparable by identity" and "the equality the domain needs." **The fix:** key by the *value* (the struct itself is comparable since all fields are comparable), so structural equality drives dedup:func dedupe(ids []*UserID) []*UserID {
seen := map[UserID]bool{} // keyed by VALUE
out := []*UserID{}
for _, id := range ids {
if !seen[*id] {
seen[*id] = true
out = append(out, id)
}
}
return out
}
String a = new String("acme/42");
String b = new String("acme/42");
System.out.println(a == b); // false — different objects (identity)
System.out.println(a.equals(b)); // true — same characters (structural)
// In a HashSet you must override equals()/hashCode() or dedup compares identity.
record UserId(String tenant, String local) {} // records auto-generate value equals/hashCode
new HashSet<>(List.of(new UserId("acme","42"), new UserId("acme","42"))).size(); // 1
Snippet 12 — TypedDict / cast lets str masquerade as int (Python)¶
Difficulty: ⭐⭐⭐ Subtle
from typing import TypedDict, cast
import csv
class Sale(TypedDict):
product: str
quantity: int
price: float
def load_sales(path: str) -> list[Sale]:
rows: list[Sale] = []
with open(path) as f:
for row in csv.DictReader(f):
rows.append(cast(Sale, row)) # csv gives all-str dicts
return rows
def revenue(sales: list[Sale]) -> float:
return sum(s["quantity"] * s["price"] for s in sales)
# sales.csv:
# product,quantity,price
# widget,3,9.99
print(revenue(load_sales("sales.csv"))) # ?
What's wrong?
Answer
`csv.DictReader` yields dicts whose **values are all strings** — `{"product": "widget", "quantity": "3", "price": "9.99"}`. `cast(Sale, row)` tells the type checker "treat this as a `Sale`" without converting anything, so `s["quantity"]` is statically `int` and `s["price"]` is statically `float`, but at runtime both are `str`. `s["quantity"] * s["price"]` is `"3" * "9.99"`: (And had both been the *same* kind — e.g. `"3" * 4` — Python would have produced a repeated-string `"3333"` instead of arithmetic, a silent corruption rather than a crash.) `mypy --strict` is green because `cast` is a pure type-checker directive with **zero runtime effect**. **Why the type system didn't catch it:** `cast(Sale, row)` is the Python equivalent of TS `as` and Java's unchecked cast — it overrides the inferred type (`dict[str, str | Any]` from `DictReader`) with the asserted one and verifies nothing. `TypedDict` describes the *intended* shape but does no parsing; it cannot conjure `int`/`float` out of CSV strings. **The fix:** convert at the parse boundary — build each `Sale` field by field with real `int`/`float` constructors, which fail loudly on bad data: Now `quantity` and `price` are genuinely numeric, `revenue` computes correctly, and a malformed cell (`"three"`) raises `ValueError` at load time instead of crashing — or silently corrupting — deep in `revenue`.Scorecard¶
Tally how the type system was defeated in each snippet. The four columns are the four ways a static type loses contact with the runtime value.
| # | Language | Defeat mechanism | Runtime symptom |
|---|---|---|---|
| 1 | TypeScript | as cast over a structural gap | undefined property → TypeError |
| 2 | TypeScript | as over JSON.parse any | wrong types: string/wrong enum, Date method missing |
| 3 | Java | raw type + unchecked (T) cast + erasure | ClassCastException at use site |
| 4 | Java | array covariance (intentional unsoundness) | ArrayStoreException |
| 5 | TypeScript | non-exhaustive switch, no never guard | silently dropped variant → wrong total |
| 6 | Python | Any leak through annotated int | TypeError from str where int expected |
| 7 | Go | interface{} erasure + bad assertion | panic: float64, not int |
| 8 | Java | autoboxing: == value vs identity, null unbox | false for cached-range miss / NPE |
| 9 | TypeScript | Partial<T> widening + as + spread semantics | undefined field / out-of-range enum |
| 10 | TypeScript | as + optional chaining + ?? default | silently wrong default, no signal |
| 11 | Go / Java | identity (== / pointer) vs structural equality | dedup keeps duplicates |
| 12 | Python | cast over CSV all-str dict | TypeError/silent string corruption |
Score yourself:
- 0–4 found: You read types as documentation. Start treating every
as,cast,(T), andinterface{}as an unverified claim — circle them on sight. - 5–8 found: Solid instincts. You spot the casts; now sharpen on the quiet ones (Snippet 10's silent default, Snippet 5's missing variant) where there is no crash to point at.
- 9–12 found: You think like the runtime, not the compiler. You know a green type check is a conditional proof — conditional on every assertion being honest.
The one rule: a type assertion (as, cast, raw (T), interface{} + .()) is a promise you must keep, because the compiler stopped checking the moment you wrote it. Keep the promise at the boundary — parse, validate, convert — or the runtime will collect on it.
Related Topics¶
- junior.md — the foundational rules these snippets violate: prefer narrow types, validate at boundaries, avoid escape hatches.
- tasks.md — hands-on exercises to practice replacing casts with validated parsing and constrained generics.
- Chapter README — the positive rules for Generics & Types, and the anti-patterns this file weaponizes.
- Anti-Patterns — broader catalog of code-level traps, including type-system abuse.
- Refactoring — systematic techniques for re-grounding lying types in reality (Replace Cast with Parse, Encapsulate Primitive).
In this topic