values that shape-shift under you
Type Errors in Dynamic Languages
Dynamic languages let any value flow anywhere; coercion and overloading turn + into concatenation and comparison into surprise. The mismatch hides until a specific input reaches a specific line.
A value's type is assumed, never checked, until an operator means something you didn't write.
01in the wild
In the wild
Coercion Makes + Ambiguous
Values shape-shift through coercion, so a + b means something different than you wrote.
example.js
// Coercion makes + ambiguous
1 + "1"; // "11" (string concat)
"1" - 1; // 0 (numeric subtraction)
[] + {}; // "[object Object]"
[] == ![]; // true (the famous one)
// RIGHT: parse at the edge, then trust your types
const n = Number(input);
if (Number.isNaN(n)) throw new Error("not a number");+ is overloaded for strings and numbers; the engine guesses. Parse input into a known type once, at the boundary.
// observed
1 + '1' = '11'
'1' - 1 = 0
parsed: Number('1') = 1 (validated)example.py
# Mixed types compare and concatenate unpredictably
"3" * 3 # '333', not 9
sorted([1, "2", 3]) # TypeError at runtime, deep in a loop
# RIGHT: type hints + a runtime guard at the edge
def area(w: float, h: float) -> float:
w, h = float(w), float(h) # coerce once, deliberately
return w * hDuck typing hides the mismatch until a specific input reaches a specific line. Coerce explicitly at the entry point.
// observed
'3' * 3 = '333'
sorted([1,'2',3]) -> TypeError
area('2','3') = 6.0 (coerced)example.rb
# nil quietly threads through everything
user = {name: "Pam"}
user[:age] + 1 # NoMethodError: undefined `+' for nil
# RIGHT: fetch with a default, or fail loudly on purpose
age = user.fetch(:age, 0)
age = user.fetch(:age) { raise "age is required" }[] returns nil for a missing key; the error surfaces far from the cause. fetch forces a decision about absence.
// observed
user[:age] + 1 -> NoMethodError on nil user.fetch(:age, 0) + 1 -> 1
example.sh
# Unquoted, unset variables are the wild west
rm -rf $DIR/ # if DIR is empty: rm -rf /
# RIGHT: fail on unset vars, quote everything
set -euo nounset
: "${DIR:?DIR must be set}"
rm -rf "${DIR:?}/" An empty variable expands to nothing, turning a cleanup into a catastrophe. nounset and quoting are non-negotiable.
// observed
unset DIR: rm -rf / (disaster) nounset: 'DIR must be set' -> script aborts
Truthiness Is Not Absence
Falsy values are not the same as missing values, but if (x) treats them identically.
example.js
// 0, "", NaN, null, undefined are ALL falsy
function setQuantity(q) {
if (!q) q = 1; // BUG: a real order of 0 becomes 1
return q;
}
// RIGHT: test for absence, not falsiness
function setQuantity(q) {
return q ?? 1; // only null / undefined fall through
}!q lumps a legitimate 0 in with null. The nullish coalescing operator distinguishes 'absent' from 'zero'.
// observed
setQuantity(0) -> 1 (wrong) q ?? 1 for 0 -> 0 (right)
example.py
def page_size(n):
return n or 50 # BUG: page_size(0) -> 50
# RIGHT: be explicit about None vs a falsy-but-valid value
def page_size(n):
return 50 if n is None else nn or 50 fires for 0, [], and '' too. 'is None' asks the question you actually meant.
// observed
page_size(0) -> 50 (wrong) is None form -> 0 (right)
02cross-pollination
Where this compounds
Runtime Errors
- Type Confusion / Bad Coercion × this Mutation
- Aliased-Default Type Confusion × Aliasing & Mutable Defaults
Data Corruption
- Type-Confusion Memory Corruption × Pointer Mismanagement
- Lossy Coercion Poisons a Shared Ledger × Threading & Mutexes
- Signed/Unsigned Conversion Poisons a Shared Aggregate × Resource Contention
03weakness catalog
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.