Stop Using any: A Type Guard Primer
Your linter is right to complain about any. Here's why unknown is the correct choice at type boundaries, and how a single isRecord guard can replace dozens of unsafe casts.
Stop Using any: A Type Guard Primer
Your linter is right to complain about any. Every any in your codebase is a hole in the type system -- a place where TypeScript stops helping you and runtime bugs start hiding.
The fix isn't complicated. It's a shift in thinking.
The Problem with any
When you write any, you're telling the compiler: "Trust me, I know what this is."
Except you usually don't. Not at boundaries.
1// This compiles. It also crashes at runtime.2class="text-category-tech font-medium">function loadConfig(raw: any) {3 class="text-category-tech font-medium">return raw.plugins.map((p: any) => p.name.toUpperCase());4}56// raw could be null, a string, a number...7// raw.plugins could be undefined8// p.name could be missingEvery property access on any is an unchecked assumption. Chain three of them and you've written code that's more fragile than plain JavaScript -- because you've told everyone (including yourself) that the types are handled.
They're not.
Use unknown at Boundaries
unknown is the type-safe counterpart to any. It means: "I don't know what this is yet, and TypeScript won't let me pretend I do."
Use unknown when:
- Data comes from JSON (config files, network responses, filesystem reads)
- Plugin configuration is arbitrary and defined by third parties
- Function arguments arrive from external callers you don't control
- Deserialized data from localStorage, databases, or message queues
1// This won't compile until you narrow the type2class="text-category-tech font-medium">function loadConfig(raw: unknown) {3 // raw.plugins <-- Error: Object is of type 'unknown'4 // You MUST validate first5}That compiler error is a feature. It forces you to prove the shape of the data before you use it.
Never "Use" unknown Directly
The key insight: unknown is a gate, not a destination. You never operate on unknown values. You narrow them first through one of two strategies:
Both work. Choose based on complexity.

Type narrowing: from unknown chaos to validated structure
The isRecord Guard
This single function eliminates an entire class of bugs:
1class="text-category-tech font-medium">function isRecord(v: unknown): v is Record { 2 class="text-category-tech font-medium">return typeof v === class="text-category-music">'object' && v !== null && !Array.isArray(v);3}Three checks. That's all it takes:
typeof v === 'object'-- eliminates primitives (strings, numbers, booleans)v !== null-- eliminatesnull(which is typeof'object'in JavaScript's oldest bug)!Array.isArray(v)-- eliminates arrays (which are also typeof'object')
What's left? An actual object with string keys. Now you can safely access properties.
Composing Guards
Once you have isRecord, you build upward:
1class="text-category-tech font-medium">function isRecord(v: unknown): v is Record { 2 class="text-category-tech font-medium">return typeof v === class="text-category-music">'object' && v !== null && !Array.isArray(v);3}45class="text-category-tech font-medium">function isPluginConfig(v: unknown): v is { name: string; options: Record } { 6 class="text-category-tech font-medium">if (!isRecord(v)) class="text-category-tech font-medium">return false;7 class="text-category-tech font-medium">if (typeof v.name !== class="text-category-music">'string') class="text-category-tech font-medium">return false;8 class="text-category-tech font-medium">if (class="text-category-music">'options' in v && !isRecord(v.options)) class="text-category-tech font-medium">return false;9 class="text-category-tech font-medium">return true;10}1112class="text-category-tech font-medium">function loadConfig(raw: unknown) {13 class="text-category-tech font-medium">if (!isRecord(raw)) {14 throw new Error(class="text-category-music">'Config must be an object');15 }1617 class="text-category-tech font-medium">if (!Array.isArray(raw.plugins)) {18 throw new Error(class="text-category-music">'Config must have a plugins array');19 }2021 class="text-category-tech font-medium">const plugins = raw.plugins.filter(isPluginConfig);2223 class="text-category-tech font-medium">return plugins.map(p => p.name.toUpperCase()); // fully typed, fully safe24}No any. No unsafe casts. TypeScript narrows the type at each step and every property access is guaranteed.
Schema Validation for Complex Shapes
When structures get deep, a schema library scales better than hand-rolled guards:
1import { z } from class="text-category-music">'zod';23class="text-category-tech font-medium">const PluginSchema = z.object({4 name: z.string(),5 options: z.record(z.unknown()).optional(),6});78class="text-category-tech font-medium">const ConfigSchema = z.object({9 plugins: z.array(PluginSchema),10 output: z.string().default(class="text-category-music">'./dist'),11 verbose: z.boolean().default(false),12});1314type Config = z.infer; 1516class="text-category-tech font-medium">function loadConfig(raw: unknown): Config {17 class="text-category-tech font-medium">return ConfigSchema.parse(raw);18 // Throws ZodError with detailed messages if invalid19 // Returns fully typed Config if valid20}Same principle. The input is unknown. The output is fully typed. The boundary is explicit.
When to Use Which
- Type guards for shallow checks, hot paths, and zero-dependency contexts
- Schema validation for complex nested structures, user input, API responses, and config files
- Both together when you want fast guards for common cases and thorough validation for the rest
Rule of Thumb
If you're checking more than 3 properties, reach for a schema library. If it's a single shape check, a type guard is cleaner.
The any Escape Hatches (When They're Legitimate)
There are rare cases where any is defensible:
- Generic library internals where you're building type utilities and the outer API is fully typed
- Third-party type workarounds when a library's types are wrong and a PR is pending
- Gradual migration from JavaScript where you're adding types incrementally
Even then, contain it. Use // eslint-disable-next-line @typescript-eslint/no-explicit-any with a comment explaining why.
1// TEMPORARY: lib@3.2.0 types are wrong for this overload2// PR: https://github.com/example/lib/pull/12343// eslint-disable-next-line @typescript-eslint/no-explicit-any4class="text-category-tech font-medium">const result = lib.transform(input as any);Document it. Link the issue. Revisit it.
The Typing Rule
At every boundary where data enters your system -- network, filesystem, user input, config, plugins -- the type is unknown. Narrow it explicitly with a guard or a schema. Never cast through any to skip the work.
Every any you remove is a runtime crash you'll never have to debug. Start with isRecord. Build from there.