Audit code for loose types that allow semantically invalid values to compile. Introduces newtypes, checked casts, enums over booleans, and documents wire/DB discriminant contracts. Use when writing structs with bare primitives, reviewing database or protocol code, or when a function takes 2+ parameters of the same type.
Make invalid states unrepresentable. All possible types and states should be defined upfront ahead of time. Every bare primitive that carries domain meaning is a bug waiting for a call site to swap two arguments of the same type. The compiler/type-checker is free; runtime debugging is not!
If a value means different things in different parameters of the same function, wrap each in a distinct type.
Rust:
struct AccountId(u64);
struct Cents(u64);
fn transfer(from: AccountId, to: AccountId, amount: Cents);TypeScript:
type AccountId = string & { readonly __brand: "AccountId" };
type Cents = number & { readonly __brand: "Cents" };
function transfer(from: AccountId, to: AccountId, amount: Cents): void;Python:
from typing import NewType
AccountId = NewType("AccountId", int)
Cents = NewType("Cents", int)
def transfer(from_: AccountId, to: AccountId, amount: Cents) -> None: ...Go:
type AccountId uint64
type Cents uint64
func Transfer(from, to AccountId, amount Cents) errorApply when a bare primitive appears in 3+ function signatures with different semantics. Common candidates: identifiers, indices, offsets, sizes, timestamps, currency amounts, ports, sequence numbers.
When converting between signed/unsigned or narrowing width, validate at the boundary where untrusted data enters. Silent truncation or wraparound is a corruption vector.
// Bad: silent wraparound on corrupt input.
let id = row.get::<_, i64>(0)? as u64;
// Good: fails loud on negative.
let id = u64::try_from(raw).map_err(|_| Error::Corrupt(raw))?;# Bad: silently wraps on overflow.
port = int(raw_value) & 0xFFFF
# Good: explicit bounds check.
port = int(raw_value)
if not (0 <= port <= 65535):
raise ValueError(f"port out of range: {port}")// Bad: silent truncation.
port := uint16(rawPort)
// Good: bounds check.
if rawPort < 0 || rawPort > math.MaxUint16 {
return fmt.Errorf("port out of range: %d", rawPort)
}The same principle applies to any narrowing conversion: i64 to i32, float64 to float32, string to fixed-length field.
A function with two boolean parameters is a trap. Replace with named alternatives that document intent at the call site.
enum Tls { On, Off }
enum CertVerify { Strict, Skip }
fn connect(host: &str, tls: Tls, verify: CertVerify);type Tls = "on" | "off";
type CertVerify = "strict" | "skip";
function connect(host: string, tls: Tls, verify: CertVerify): void;class Tls(enum.Enum):
ON = "on"
OFF = "off"
class CertVerify(enum.Enum):
STRICT = "strict"
SKIP = "skip"
def connect(host: str, tls: Tls, verify: CertVerify) -> None: ...If an enum's numeric values or string tags are stored in a database or sent over the wire, document that changing them is a breaking migration. This applies equally to Rust #[repr(u8)], TypeScript string unions, Python IntEnum, and Go iota constants.
These former standalone skills are bundled here as references to keep the runtime list compact. Load only the reference that matches the user's exact product, framework, or failure mode.
| Former skill | Reference | Description |
|---|---|---|
dedalus-fix-types | references/skills/dedalus-fix-types/SKILL.md | Audit code for loose types that allow semantically invalid values to compile. Introduces newtypes, checked casts, enums over booleans, and documents wire/DB discriminant contracts. Use when writing structs with bare primitives, reviewing database or protocol code, or when a function takes 2+ parameters of the same type. |