Claude-skill-registry avoiding-any-types
Teaches when and how to use unknown instead of any type in TypeScript. Use when working with TypeScript code that has any types, needs type safety, handling external data, or when designing APIs. Critical for preventing type safety violations.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/avoiding-any-types" ~/.claude/skills/majiayu000-claude-skill-registry-avoiding-any-types && rm -rf "$T"
skills/data/avoiding-any-types/SKILL.md- Code contains
type annotationsany - Working with external data (APIs, JSON, user input)
- Designing generic functions or types
- User mentions type safety, type checking, or avoiding
any - Files contain patterns like
,: any
,<any>
</when-to-activate>as any
The
unknown type is any's type-safe counterpart. It accepts any value like any, but requires type narrowing before use.
Key Pattern:
any → unknown → type guard → safe access
Impact: Prevents runtime errors while maintaining flexibility for truly dynamic data. </overview>
<workflow> ## Decision FlowStep 1: Identify
Usageany
When you see
any, ask:
- Is this truly unknowable at compile time? (external data, plugin systems)
- Can I use a more specific type? (union types, generics with constraints)
- Is this laziness or necessity?
Step 2: Choose Replacement Strategy
Strategy A: Use Specific Types (preferred)
- Known structure → Use interfaces/types
- Multiple possible types → Use union types
- Shared shape → Use generics with constraints
Strategy B: Use
(when truly dynamic)unknown
- External data with unknown structure
- Plugin/extension systems
- Gradual migration from JavaScript
Strategy C: Keep
(rare, justified cases)any
- Interop with poorly-typed libraries
- Complex type manipulation that TypeScript can't express
- Performance-critical code where type checks are prohibitive
Step 3: Implement Type Guards
When using
unknown, implement type guards:
- Runtime validation (Zod, io-ts, custom validators)
- Type predicates for custom guards
- Built-in guards (typeof, instanceof, in) </workflow>
For detailed patterns and examples:
- Type Guard Patterns: Use the using-type-guards skill for comprehensive type guard implementation
- Runtime Validation: Use the using-runtime-checks skill for validating unknown data
- Generic Constraints: Use the using-generics skill for constraining generic types </progressive-disclosure>
❌ Using
(unsafe)any
async function fetchUser(id: string): Promise<any> { const response = await fetch(`/api/users/${id}`); return response.json(); } const user = await fetchUser("123"); console.log(user.name.toUpperCase());
Problem: If API returns
{ username: string } instead of { name: string }, this crashes at runtime. TypeScript provides no protection.
✅ Using
+ validation (safe)unknown
async function fetchUser(id: string): Promise<unknown> { const response = await fetch(`/api/users/${id}`); return response.json(); } function isUser(value: unknown): value is { name: string } { return ( typeof value === "object" && value !== null && "name" in value && typeof value.name === "string" ); } const userData = await fetchUser("123"); if (isUser(userData)) { console.log(userData.name.toUpperCase()); } else { throw new Error("Invalid user data"); }
Better: Use Zod for complex validation (use the using-runtime-checks skill)
Example 2: Generic Function Defaults
❌ Using
in generic default (unsafe)any
interface ApiResponse<T = any> { data: T; status: number; } const response: ApiResponse = { data: "anything", status: 200 }; response.data.nonexistent.property;
Problem: Generic defaults to
any, losing all type safety.
✅ Using
default (safe)unknown
interface ApiResponse<T = unknown> { data: T; status: number; } const response: ApiResponse = { data: "anything", status: 200 }; if (typeof response.data === "string") { console.log(response.data.toUpperCase()); }
Even Better: Require explicit type parameter
interface ApiResponse<T> { data: T; status: number; } const response: ApiResponse<User> = await fetchUser();
Example 3: Error Handling
❌ Using
for caught errors (unsafe)any
try { await riskyOperation(); } catch (error: any) { console.log(error.message); }
Problem: Not all thrown values are Error objects. This crashes if someone throws a string or number.
✅ Using
+ type guard (safe)unknown
try { await riskyOperation(); } catch (error: unknown) { if (error instanceof Error) { console.log(error.message); } else { console.log("Unknown error:", String(error)); } }
Example 4: Validation Function
❌ Using
parameter (unsafe)any
function validate(data: any): boolean { return data.email && data.password; }
Problem: Typos like
data.emial are not caught. No autocomplete support.
✅ Using
+ type guard (safe)unknown
</examples> <constraints> **MUST:**interface LoginData { email: string; password: string; } function isLoginData(data: unknown): data is LoginData { return ( typeof data === "object" && data !== null && "email" in data && "password" in data && typeof data.email === "string" && typeof data.password === "string" ); } function validate(data: unknown): data is LoginData { if (!isLoginData(data)) { return false; } return data.email.includes("@") && data.password.length >= 8; }
- Use
for external data (APIs, JSON.parse, user input)unknown - Use
for generic defaults when type is truly dynamicunknown - Implement type guards before accessing
valuesunknown - Use runtime validation libraries (Zod, io-ts) for complex structures
SHOULD:
- Prefer specific types (interfaces, unions) over
when structure is knownunknown - Use type predicates (
) for reusable type guardsvalue is Type - Narrow
progressively (check object → check properties → check types)unknown
NEVER:
- Use
for external dataany - Use
to silence TypeScript errorsas any - Use
in public API surfacesany - Default generics to
any - Cast
to specific types without validation </constraints>unknown
After replacing
any with unknown:
-
Type Guard Exists:
- Every
value has a corresponding type guardunknown - Type guards check structure AND types
- Guards return early on invalid data
- Every
-
Safe Access Only:
- No property access before type narrowing
- No method calls before type narrowing
- IDE autocomplete works after narrowing
-
Error Handling:
- Invalid data handled gracefully
- Clear error messages
- No silent failures
-
Compilation:
npx tsc --noEmitShould pass without
suppressions. </validation>any
1. Library Interop (temporary)
import poorlyTypedLib from "poorly-typed-lib"; const result = poorlyTypedLib.method() as any;
Consider contributing types to DefinitelyTyped or wrapping in typed facade.
2. Type Manipulation Edge Cases
type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]; };
Sometimes TypeScript's type system can't express complex patterns. Document why.
3. Explicit Opt-Out
const configSchema: any = generateFromSpec();
Explicitly choosing to skip type checking for this value. Document decision. </common-patterns>
<migration-path> ## Migrating Existing `any` UsagePhase 1: Audit
grep -rn ": any" src/ grep -rn "<any>" src/ grep -rn "= any" src/
Phase 2: Classify
- External data →
+ validationunknown - Known structure → specific types
- Generic defaults → remove default or use
unknown - Justified
→ document with comment explaining whyany
Phase 3: Replace
- Start with external boundaries (API layer, JSON parsing)
- Work inward toward core logic
- Add tests to verify runtime behavior unchanged
Phase 4: Prevent
- Enable
in tsconfig.jsonnoImplicitAny - Add lint rules forbidding
any - Use hooks to catch new
introduction </migration-path>any