soundness-traps
npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill soundness-traps
Agent 安装分布
Skill 文档
Avoid Soundness Traps
Overview
TypeScript is not sound – runtime values can diverge from static types.
“Soundness” means static types always match runtime values. TypeScript intentionally trades some soundness for convenience. Know the common traps.
When to Use This Skill
- Debugging “impossible” runtime errors
- Understanding TypeScript’s limitations
- Writing defensive code
- Evaluating trade-offs of strict options
The Iron Rule
TypeScript types are NOT runtime guarantees.
Know the common soundness traps and avoid them.
Remember:
- Array access doesn’t check bounds
- Type assertions bypass checking
- Functions can mutate their parameters
- External data may not match declared types
Soundness vs Convenience
TypeScript chooses convenience over soundness in many cases:
const xs = [1, 2, 3];
const x = xs[10];
// ^? number (but actually undefined!)
This is unsound but convenient. Checking bounds at every access would be tedious.
Common Soundness Traps
1. Unchecked Array Access
const arr = [1, 2, 3];
const item = arr[5]; // undefined at runtime
// ^? number (wrong!)
item.toFixed(2); // Crashes!
Fix: Use noUncheckedIndexedAccess or check explicitly:
const item = arr[5];
if (item !== undefined) {
item.toFixed(2); // OK
}
2. Type Assertions
const hour = (new Date()).getHours() || null;
// ^? number | null
// Assertion removes null
const definitelyHour = hour as number;
// ^? number (but might be null!)
Fix: Use conditionals instead of assertions:
if (hour !== null) {
hour.toFixed(1); // TypeScript knows it's number
}
3. any Types
function log(x: number) {
console.log(x.toFixed(1));
}
const val: any = 'not a number';
log(val); // No error, crashes at runtime
Fix: Avoid any. Use unknown with narrowing:
function log(x: unknown) {
if (typeof x === 'number') {
console.log(x.toFixed(1));
}
}
4. Object Index Access
type Dict = { [key: string]: string };
const dict: Dict = { a: 'apple' };
const val = dict['b'];
// ^? string (but actually undefined!)
Fix: Include undefined in the type:
type Dict = { [key: string]: string | undefined };
const val = dict['b'];
// ^? string | undefined
5. Function Parameter Mutation
function addFox(animals: Animal[]) {
animals.push(new Fox());
}
const hens: Hen[] = [new Hen()];
addFox(hens); // Fox in the henhouse!
Fix: Use readonly to prevent mutation:
function addFox(animals: readonly Animal[]) {
animals.push(new Fox());
// ~~~~ Property 'push' does not exist
}
6. Refinements Invalidated by Callbacks
interface Data {
value?: string;
}
function process(data: Data, callback: (d: Data) => void) {
if (data.value) {
callback(data);
console.log(data.value.toUpperCase()); // Might crash!
// ^? string (but callback might have deleted it!)
}
}
process({ value: 'hello' }, d => delete d.value);
Fix: Capture the value before the callback:
function process(data: Data, callback: (d: Data) => void) {
const value = data.value;
if (value) {
callback(data);
console.log(value.toUpperCase()); // Safe!
}
}
7. Inaccurate Type Declarations
// Library types might be wrong
declare function getUser(): { name: string; email: string };
const user = getUser();
user.email.toLowerCase(); // Might crash if email is undefined!
Fix: Validate external data at runtime:
const user = getUser();
if (user.email) {
user.email.toLowerCase();
}
8. Optional Properties and Assignability
interface Person { name: string; }
interface AgePerson { name: string; age?: number; }
const p: Person = { name: 'Bob', age: '30' }; // age is string
const ap: AgePerson = p; // No error!
console.log(ap.age?.toFixed()); // toFixed on string!
This is a subtle unsoundness from TypeScript’s structural typing.
Compiler Options for Soundness
| Option | What It Catches |
|---|---|
strictNullChecks |
null/undefined assignments |
noUncheckedIndexedAccess |
Array/object access returning undefined |
strictFunctionTypes |
Function parameter contravariance |
Enable these for more safety (at cost of convenience).
General Strategies
- Validate external data (APIs, JSON, user input)
- Use readonly for parameters you don’t mutate
- Prefer unknown to any
- Avoid type assertions; use narrowing instead
- Enable strict compiler options
- Test edge cases (empty arrays, missing properties)
Pressure Resistance Protocol
1. “TypeScript Should Catch This”
Pressure: “Why didn’t TypeScript catch this bug?”
Response: TypeScript is intentionally unsound in many cases.
Action: Know the traps; write defensive code.
2. “It’s Too Strict”
Pressure: “noUncheckedIndexedAccess is annoying”
Response: It catches real bugs. The friction is worth it.
Action: Enable strict options; handle the edge cases.
Red Flags – STOP and Reconsider
- Accessing array elements without bounds checking
- Type assertions to “fix” type errors
- Mutating function parameters
- Trusting external data matches declared types
Common Rationalizations (All Invalid)
| Excuse | Reality |
|---|---|
| “TypeScript will catch it” | TypeScript is not sound |
| “It always has a value” | Until it doesn’t |
| “The API is well-documented” | Docs lie; validate |
Quick Reference
// TRAP: Array access
const x = arr[10]; // Might be undefined
// TRAP: Type assertion
const y = val as number; // Might not be number
// TRAP: any type
const z: any = 'string';
z.toFixed(); // Crashes
// TRAP: Index signature
const v = dict['missing']; // Undefined
// SAFE: Narrowing
if (typeof val === 'number') { val.toFixed(); }
// SAFE: Optional chaining
arr[10]?.toFixed();
// SAFE: undefined in type
type Dict = { [k: string]: string | undefined };
The Bottom Line
TypeScript is convenient, not sound.
Know where types can diverge from runtime values. Enable strict options. Validate external data. Write defensive code. Don’t trust types blindly.
Reference
Based on “Effective TypeScript” by Dan Vanderkam, Item 48: Avoid Soundness Traps.