Handling Implicit Any and Type Errors

9 min read

As you rename files to .ts, you will encounter a recurring set of TypeScript errors. Most of them are variations on the same theme: TypeScript is asking "what is this, exactly?" and the JavaScript code doesn't have an answer. This lesson covers the errors you'll see most often, how to diagnose each one, and the right fix — not just the quickest fix.

Implicit any — TypeScript's most common migration error

When a function parameter has no type annotation and TypeScript cannot infer it from context, it flags the parameter as having an implicit any type. With noImplicitAny: true, this is an error. With noImplicitAny: false (the migration-safe default), it's a warning or silent — but the parameter is still typed as any, which disables all type checking on it.

// ERROR with noImplicitAny: true
function processResponse(data) {
//                        ^^^^ Parameter 'data' implicitly has an 'any' type
  return data.body.users;
}

The fix hierarchy, from best to worst:

1. Add an explicit type (always preferred):

interface ApiResponse {
  body: { users: User[] };
}
function processResponse(data: ApiResponse): User[] {
  return data.body.users;
}

2. Use unknown when the shape is genuinely variable:

function processResponse(data: unknown): User[] {
  if (
    typeof data === 'object' &&
    data !== null &&
    'body' in data &&
    typeof (data as { body: unknown }).body === 'object'
  ) {
    return (data as ApiResponse).body.users;
  }
  throw new Error('Unexpected API response shape');
}

3. Use explicit any as a tracked placeholder:

// TODO: type this properly — see #456
function processResponse(data: any): User[] {
  return data.body.users;
}

Explicit : any is better than implicit any because it is visible in code review, searchable (grep -rn ": any"), and communicates intent. Implicit any is silent.

The five errors you'll see most

Object is possibly undefined

Array.prototype.find(), Map.prototype.get(), and many DOM APIs return T | undefined. Without strictNullChecks, TypeScript lets this through. With it enabled, you must handle the undefined case.

const user = users.find(u => u.id === targetId);
console.log(user.email);
// ERROR: Object is possibly 'undefined'

Three valid fixes:

// Fix 1: guard with if
if (user) {
  console.log(user.email); // user is User here
}
 
// Fix 2: throw on not-found (best for test helpers where missing data = test bug)
const user = users.find(u => u.id === targetId);
if (!user) throw new Error(`User ${targetId} not found in fixture`);
console.log(user.email); // narrowed to User
 
// Fix 3: optional chaining (only when undefined is a valid, silent case)
console.log(user?.email ?? 'unknown');

In test code, Fix 2 is almost always right. A missing fixture user means the test setup is wrong — that should fail loudly, not silently return undefined and produce a confusing assertion failure later.

Property does not exist on type

interface TestUser {
  id: string;
  email: string;
  role: string;
}
 
const user = createTestUser();
console.log(user.eamil);
// ERROR: Property 'eamil' does not exist on type 'TestUser'. Did you mean 'email'?

This is TypeScript catching a real bug. Fix the typo. If the property genuinely doesn't exist on the interface yet, add it to the interface rather than working around the error.

Argument of type X is not assignable

function setRole(role: 'admin' | 'member'): void { ... }
 
const role = 'superadmin';
setRole(role);
// ERROR: Argument of type 'string' is not assignable to parameter of type '"admin" | "member"'

This happens when you pass a string where a union literal is expected. TypeScript widens the variable's type to string at declaration. Fix with as const:

const role = 'superadmin' as const; // type: 'superadmin' — but this is still wrong, fix the value
const role = 'admin' as const;      // type: 'admin' — correct
setRole(role); // OK

Or annotate the variable:

const role: 'admin' | 'member' = 'admin';

Type narrowing during migration

Narrowing is TypeScript's technique for refining a broad type to a specific one within a conditional block. You'll use it constantly when handling unknown values from API responses, JSON fixtures, and dynamic imports.

function handleApiResult(result: unknown): string {
  // typeof narrowing — for primitives
  if (typeof result === 'string') {
    return result.toUpperCase(); // result is string here
  }
 
  // truthiness narrowing — for null/undefined
  if (!result) {
    throw new Error('Empty result');
  }
 
  // in narrowing — for object property checks
  if (typeof result === 'object' && 'message' in result) {
    return (result as { message: string }).message;
  }
 
  return String(result);
}

In Playwright tests, narrowing is common when dealing with page.evaluate() return values:

const count = await page.evaluate(() => document.querySelectorAll('.item').length);
// count is number — evaluate infers the return type from the callback

Type assertions — using them safely

A type assertion (as SomeType) tells TypeScript to treat a value as a specific type. It skips the type check entirely. Use it only when you have external evidence that the assertion is correct and the compiler cannot verify it.

// Good use — you know the selector always returns this element type
const input = document.querySelector('#email') as HTMLInputElement;
input.value = 'alice@test.com';
 
// Good use — JSON.parse result typed against a known schema
const fixture = JSON.parse(fs.readFileSync('users.json', 'utf8')) as { users: TestUser[] };
 
// Bad use — hiding a real type mismatch
const user = getUser(id) as User; // if getUser returns User | null, this masks null

Prefer narrowing over assertions when the runtime value is genuinely uncertain. Use assertions when you're bridging TypeScript's type system and a well-understood external contract (a DOM API, a JSON file with a known schema, a Playwright evaluate result).

Tracking and reducing your any budget

As migration progresses, track explicit any usage with a simple search:

grep -rn ": any\|as any" src/ --include="*.ts" | wc -l

Run this weekly. The number should trend toward zero. Files with a high any count are candidates for the next migration round. A project that's "migrated to TypeScript" but has 400 remaining any annotations has TypeScript's build-step cost without most of its safety.

⚠️ Common mistakes

  • Using as any to silence every error. const result = something as any is an ejector seat — it turns TypeScript off for every subsequent operation on result. Use unknown instead: it forces you to check before using, which is the point.
  • Suppressing the null-check error instead of fixing the root cause. user!.email (non-null assertion) is appropriate only when you have already verified, at the call site, that user is non-null. Using it because "I know this can't be null" is usually wrong — it's the same claim the JavaScript developer made before getting TypeError: Cannot read properties of undefined.
  • Not reading the full error message. TypeScript error messages are long but precise. The last line often names exactly which field is wrong or which type is mismatched. Most TypeScript beginners read the first line and guess at the fix. Read the whole message.

🎯 Practice task

Take the most complex file you've converted so far.

  1. Run npm run type-check and list all errors. Categorise them: implicit any, possibly undefined, property does not exist, type mismatch.
  2. For every implicit any error: choose between an explicit type, unknown with narrowing, or explicit any with a TODO comment. Apply the fix.
  3. For every "possibly undefined" error: decide between a null guard that throws, optional chaining, or widening the return type. Apply the fix that best matches the context (test helper = throw; optional data = optional chaining).
  4. Run grep -rn ": any" . --include="*.ts" on your project. Count the explicit any annotations. Write the number down. This is your baseline.
  5. Stretch: find one place where you used as SomeType and ask whether it's truly safe. Is the assertion correct at runtime in all cases? If not, replace it with a runtime check that throws on unexpected shapes.

// tip to track lessons you complete and pick up where you left off across devices.